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/07/10 06:55:02 UTC

(hertzbeat) branch 2.0.0 updated: feat: complete HertzBeat 2.0 novice workflows

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


The following commit(s) were added to refs/heads/2.0.0 by this push:
     new 2de48d2caa feat: complete HertzBeat 2.0 novice workflows
2de48d2caa is described below

commit 2de48d2caa43fa58fd93e71b0e22e7d4f431c9d5
Author: Logic <zq...@dromara.org>
AuthorDate: Fri Jul 10 14:54:43 2026 +0800

    feat: complete HertzBeat 2.0 novice workflows
    
    Refine entity, monitor, alert, settings, observability, and shared operator workflows; add scale guards, route compatibility, release validation, and Product Design coverage.
    
    AI assistance: used for draft implementation and test iteration.\nHuman validation: Node 22 production build; 426 frontend files and 3,398 tests; 59-route matrix; release budget 7.84 MiB / 8.00 MiB; focused backend tests and Browser QA.\nRisk notes: local scale proof covers 2,005 entities; sustained concurrent-ingestion SLA remains deployment-specific.
---
 docs/product-design-user-validation-loop.md        |  214 ++
 .../alert/service/impl/AlertDefineServiceImpl.java |    2 +-
 .../dto/entity/EntityOpsSummaryInfo.java           |   30 +-
 .../manager/controller/EntityController.java       |   18 +
 .../apache/hertzbeat/manager/dao/BulletinDao.java  |    5 +
 .../hertzbeat/manager/dao/EntityIdentityDao.java   |    4 +
 .../manager/dao/EntityMonitorBindDao.java          |    6 +
 .../hertzbeat/manager/dao/EntityRelationDao.java   |   16 +
 .../hertzbeat/manager/dao/ObserveEntityDao.java    |    4 +
 .../manager/service/ObserveEntityService.java      |    3 +
 .../service/entity/EntityCatalogQueryService.java  |   88 +-
 .../EntityDefinitionDocumentParserService.java     |  130 +-
 .../entity/EntityDefinitionDraftService.java       |  129 +-
 ...tityDefinitionRelationNormalizationService.java |    7 +-
 .../EntityDefinitionTypeResolverService.java       |   20 +-
 .../EntityDetailObservabilityReadModelService.java |   24 +-
 .../entity/EntityDetailReadModelService.java       |   26 +-
 .../service/entity/EntityIdentityQueryService.java |   15 +
 .../entity/EntityIdentityReadModelService.java     |    5 +
 .../entity/EntityIdentityWriteModelService.java    |   56 +-
 .../entity/EntityIntegrationHintService.java       |   71 +-
 .../service/entity/EntityListReadModelService.java |   50 +-
 .../entity/EntityMonitorBindQueryService.java      |   40 +
 .../service/entity/EntityMonitorBindService.java   |   60 +
 .../entity/EntityMutationWorkflowService.java      |  200 +-
 .../entity/EntityNoiseControlRuleQueryService.java |    6 +-
 .../service/entity/EntityRelationQueryService.java |   34 +
 .../service/entity/EntityRelationService.java      |    9 +
 .../service/entity/EntityStatusRefreshService.java |   61 +-
 .../entity/EntitySummaryReadModelService.java      |   67 +-
 .../entity/EntityWorkspaceAccessService.java       |   10 +
 .../entity/EntityWorkspaceQueryService.java        |    7 +
 .../entity/LocalTopologyDemoRelationSeeder.java    |   96 +-
 .../service/impl/AbstractImExportServiceImpl.java  |   21 +
 .../manager/service/impl/BulletinServiceImpl.java  |    5 +-
 .../manager/service/impl/MonitorServiceImpl.java   |   14 +-
 .../service/impl/ObserveEntityServiceImpl.java     |    7 +
 .../manager/service/impl/PluginServiceImpl.java    |   22 +-
 .../manager/service/BulletinServiceTest.java       |   44 +
 .../manager/service/JsonImExportServiceTest.java   |   72 +
 .../manager/service/MonitorServiceTest.java        |   24 +
 .../manager/service/ObserveEntityServiceTest.java  |   57 +-
 .../manager/service/PluginServiceTest.java         |   52 +-
 .../EntityDefinitionDocumentParserServiceTest.java |   42 +
 .../entity/EntityDefinitionDraftServiceTest.java   |  113 +
 .../EntityDefinitionNormalizationServiceTest.java  |   14 +
 ...DefinitionRelationNormalizationServiceTest.java |   23 +
 .../EntityDefinitionTypeResolverServiceTest.java   |   12 +
 ...tityDetailChildEvidenceSourceOwnershipTest.java |    5 +
 ...tityDetailFacadeHandoffSourceOwnershipTest.java |    9 +-
 ...ityDetailObservabilityReadModelServiceTest.java |   93 +-
 .../entity/EntityDetailReadModelServiceTest.java   |   33 +
 .../entity/EntityIdentityQueryServiceTest.java     |   11 +
 .../EntityIdentityWriteModelServiceTest.java       |   81 +-
 .../entity/EntityIntegrationHintServiceTest.java   |   81 +-
 .../EntityListCatalogQuerySourceOwnershipTest.java |   15 +
 .../entity/EntityListReadModelServiceTest.java     |   60 +-
 ...tityListRuntimeEvidenceSourceOwnershipTest.java |    3 +-
 .../entity/EntityMonitorBindQueryServiceTest.java  |   35 +
 .../entity/EntityMonitorBindServiceTest.java       |   49 +
 .../entity/EntityMutationWorkflowServiceTest.java  |  136 +
 .../EntityNoiseControlRuleQueryServiceTest.java    |    8 +-
 ...tyNoiseControlRuleQuerySourceOwnershipTest.java |    6 +
 .../entity/EntityRelationQueryServiceTest.java     |   39 +
 .../service/entity/EntityRelationServiceTest.java  |   15 +
 .../entity/EntityStatusRefreshServiceTest.java     |   46 +
 .../EntityStatusRefreshSourceOwnershipTest.java    |    3 +-
 .../entity/EntitySummaryReadModelServiceTest.java  |   59 +
 .../entity/EntityWorkspaceAccessServiceTest.java   |   19 +
 .../EntityWorkspaceQuerySourceOwnershipTest.java   |    4 +
 .../LocalTopologyDemoRelationSeederTest.java       |   22 +
 .../impl/OtlpIngestionWorkspaceServiceImpl.java    |    6 +-
 .../impl/EntityObservabilityGatewayImpl.java       |    3 +-
 .../service/impl/TelemetryIntakeServiceImpl.java   |   71 +-
 .../traces/controller/TraceQueryController.java    |    8 -
 .../service/impl/EntityTraceQueryServiceImpl.java  |   31 +-
 .../OtlpIngestionWorkspaceServiceImplTest.java     |   85 +
 .../impl/EntityObservabilityGatewayImplTest.java   |    2 +
 .../impl/TelemetryIntakeServiceImplTest.java       |  103 +
 .../controller/TraceQueryControllerTest.java       |   12 +-
 .../impl/EntityTraceQueryServiceImplTest.java      |   43 +
 web-next/app/actions/actions-page.tsx              |   22 +-
 web-next/app/actions/page.test.tsx                 |   20 +
 web-next/app/alert-family.chrome.test.ts           |   27 +-
 web-next/app/alert/alert-center-page.tsx           |   18 +-
 web-next/app/alert/group/alert-group-page.tsx      |  207 +-
 web-next/app/alert/group/page.test.tsx             |  242 +-
 web-next/app/alert/inhibit/alert-inhibit-page.tsx  |  260 +-
 web-next/app/alert/inhibit/page.test.tsx           |  280 +-
 .../[source]/alert-integration-source-redirect.tsx |   17 +
 .../app/alert/integration/[source]/page.test.tsx   |   31 +-
 web-next/app/alert/integration/[source]/page.tsx   |    7 +-
 web-next/app/alert/notice/alert-notice-page.tsx    | 1405 +++++++--
 web-next/app/alert/notice/page.test.tsx            |  488 +++-
 web-next/app/alert/page.test.tsx                   |   29 +
 web-next/app/alert/setting/alert-setting-page.tsx  |  282 +-
 web-next/app/alert/setting/page.test.tsx           |  399 ++-
 web-next/app/alert/silence/alert-silence-page.tsx  |  270 +-
 web-next/app/alert/silence/page.test.tsx           |  276 +-
 web-next/app/api/account/auth/form/route.ts        |   41 +-
 web-next/app/api/account/auth/refresh/route.ts     |    6 +-
 web-next/app/api/account/session/route.ts          |    4 +-
 .../app/compatibility-entrypoints.chrome.test.ts   |    4 +-
 .../app/dashboard/dashboard-draft-workspace.tsx    |  225 +-
 web-next/app/dashboard/page.test.ts                |   39 +-
 .../definition/entity-definition-page.tsx          |   10 +-
 .../entities/[entityId]/definition/page.test.tsx   |   27 +-
 .../app/entities/[entityId]/definition/page.tsx    |   13 +-
 .../entities/[entityId]/edit/entity-edit-page.tsx  |   78 +-
 .../app/entities/[entityId]/edit/page.test.tsx     |   87 +-
 web-next/app/entities/[entityId]/edit/page.tsx     |   13 +-
 .../app/entities/[entityId]/entity-detail-page.tsx |   51 +-
 web-next/app/entities/[entityId]/page.test.tsx     |  180 +-
 web-next/app/entities/[entityId]/page.tsx          |   11 +-
 .../entities/discovery/entity-discovery-page.tsx   |   13 +
 web-next/app/entities/discovery/page.test.tsx      |   17 +-
 web-next/app/entities/entity-list-page.tsx         |  201 +-
 .../app/entities/import/entity-import-page.tsx     |   25 +-
 web-next/app/entities/import/page.test.tsx         |   60 +-
 web-next/app/entities/import/page.tsx              |   19 +-
 web-next/app/entities/legacy/page.test.ts          |   31 +
 web-next/app/entities/legacy/page.tsx              |   12 +
 web-next/app/entities/new/entity-new-page.tsx      |   48 +-
 web-next/app/entities/new/page.test.tsx            |   61 +-
 web-next/app/entities/page.test.tsx                |  539 +++-
 web-next/app/entity-editor-family.chrome.test.ts   |    4 +-
 web-next/app/explorer/explorer-page.tsx            |   82 +-
 web-next/app/explorer/page.test.tsx                |   52 +-
 web-next/app/globals.css                           |    4 +-
 web-next/app/incidents/incidents-page.tsx          |   10 +
 web-next/app/incidents/page.test.tsx               |   17 +-
 .../ingestion/otlp/metrics/otlp-metrics-page.tsx   |   55 +-
 web-next/app/ingestion/otlp/metrics/page.test.tsx  |   44 +-
 web-next/app/ingestion/otlp/otlp-page.tsx          |  123 +-
 web-next/app/ingestion/otlp/page.test.tsx          |   36 +-
 web-next/app/loading.test.tsx                      |    9 +-
 web-next/app/loading.tsx                           |   24 +-
 web-next/app/log/manage/log-manage-page.tsx        |  279 +-
 web-next/app/log/manage/page.test.tsx              |  216 +-
 web-next/app/log/stream/page.test.tsx              |   78 +-
 web-next/app/log/stream/page.tsx                   |   12 +-
 web-next/app/milestone5-final-sweep.chrome.test.ts |   15 +-
 .../milestone5-shared-route-owners.chrome.test.ts  |   12 +-
 .../[monitorId]/edit/monitor-edit-page.tsx         |   16 +-
 .../app/monitors/[monitorId]/edit/page.test.tsx    |   12 +
 .../monitors/[monitorId]/monitor-detail-page.tsx   |   90 +-
 web-next/app/monitors/[monitorId]/page.test.tsx    |   38 +
 web-next/app/monitors/legacy/page.test.ts          |   31 +
 web-next/app/monitors/legacy/page.tsx              |   12 +
 web-next/app/monitors/monitor-manage-page.tsx      |  519 +++-
 web-next/app/monitors/new/page.test.tsx            |    5 +-
 web-next/app/monitors/page.test.tsx                |  200 +-
 web-next/app/operator-family.chrome.test.ts        |    3 +-
 web-next/app/overview/overview-page.tsx            |   39 +-
 web-next/app/overview/page.test.tsx                |    3 +
 web-next/app/passport/lock/page.test.tsx           |    5 +-
 web-next/app/passport/login/page.tsx               |    5 +-
 web-next/app/setting/collector/page.test.tsx       |  242 +-
 .../setting/collector/setting-collector-page.tsx   |   78 +-
 web-next/app/setting/define/page.test.tsx          |  270 +-
 .../app/setting/define/setting-define-page.tsx     |   76 +-
 web-next/app/setting/labels/page.test.tsx          |  284 +-
 .../app/setting/labels/setting-labels-page.tsx     |  122 +-
 web-next/app/setting/page.test.ts                  |    6 +-
 web-next/app/setting/plugins/page.test.tsx         |  212 +-
 .../app/setting/plugins/setting-plugins-page.tsx   |   76 +-
 web-next/app/setting/settings/config/page.test.tsx |  263 +-
 .../settings/config/setting-config-page.tsx        |  178 +-
 .../app/setting/settings/mcp-server/page.test.ts   |    4 +-
 web-next/app/setting/settings/mcp-server/page.tsx  |    9 +-
 .../setting/settings/object-store/page.test.tsx    |  249 +-
 .../object-store/setting-object-store-page.tsx     |  221 +-
 web-next/app/setting/settings/page.test.ts         |    6 +-
 web-next/app/setting/settings/server/page.test.tsx |  534 +++-
 .../settings/server/setting-server-page.tsx        |  451 ++-
 web-next/app/setting/settings/token/page.test.tsx  |  384 ++-
 .../setting/settings/token/setting-token-page.tsx  |  380 ++-
 web-next/app/setting/status/page.test.tsx          |  196 +-
 web-next/app/setting/status/page.tsx               |   10 +-
 .../app/setting/status/setting-status-page.tsx     |   56 +-
 web-next/app/settings-family.chrome.test.ts        |   13 +-
 web-next/app/three-signal-family.chrome.test.ts    |   10 +-
 web-next/app/topology/page.test.tsx                |  159 +-
 web-next/app/topology/topology-page.tsx            |  274 +-
 .../app/topology/topology-route-client.test.tsx    |  120 +
 web-next/app/trace/manage/page.test.tsx            |   96 +-
 web-next/app/trace/manage/route-state.test.ts      |   17 +
 web-next/app/trace/manage/route-state.ts           |    3 +-
 .../app/trace/manage/trace-manage-client.test.tsx  |  177 ++
 web-next/app/trace/manage/trace-manage-page.tsx    |  249 +-
 web-next/app/ui-lab/page.tsx                       |    2 +-
 .../log-manage/log-stream-detail-dialog.test.tsx   |    1 +
 .../log-manage/log-stream-detail-dialog.tsx        |    1 +
 .../monitor-detail/monitor-detail-console.test.tsx |   19 +-
 .../monitor-detail/monitor-detail-console.tsx      |   60 +-
 .../monitor-detail-sections.test.tsx               |  152 +
 .../monitor-detail/monitor-detail-sections.tsx     |   78 +-
 .../monitor-history-chart-grid.test.tsx            |    8 +-
 .../monitor-detail/monitor-history-chart-grid.tsx  |    6 +-
 .../monitor-detail/monitor-history-panel.test.tsx  |   13 +-
 .../observability/observability.chrome.test.ts     |    7 +-
 .../observability/time-range-control.test.tsx      |   10 +-
 .../observability/time-range-control.tsx           |   29 +-
 web-next/components/observability/toolbar.test.tsx |    6 +-
 .../components/overview/overview-console.test.tsx  |   19 +
 web-next/components/overview/overview-console.tsx  |    6 +-
 .../pages/alert-authoring-primitives.test.tsx      |   43 +
 .../pages/alert-authoring-primitives.tsx           |   76 +-
 .../pages/alert-authoring.chrome.test.ts           |    4 +-
 .../components/pages/alert-center-surface.test.tsx |  128 +-
 web-next/components/pages/alert-center-surface.tsx |  141 +-
 .../pages/alert-group-authoring-fields.test.tsx    |   23 +
 .../pages/alert-group-authoring-fields.tsx         |   75 +-
 .../components/pages/alert-group-surface.test.tsx  |   89 +
 web-next/components/pages/alert-group-surface.tsx  |  145 +-
 .../pages/alert-inhibit-authoring-fields.test.tsx  |   25 +
 .../pages/alert-inhibit-authoring-fields.tsx       |   92 +-
 .../pages/alert-inhibit-surface.test.tsx           |  115 +
 .../components/pages/alert-inhibit-surface.tsx     |  187 +-
 .../pages/alert-integration-markdown.test.tsx      |   32 +-
 .../components/pages/alert-integration-mermaid.tsx |  160 +-
 .../pages/alert-notice-receiver-fields.test.tsx    |  122 +-
 .../pages/alert-notice-receiver-fields.tsx         |  228 +-
 .../pages/alert-notice-rule-fields.test.tsx        |  112 +-
 .../components/pages/alert-notice-rule-fields.tsx  |  237 +-
 .../pages/alert-notice-template-fields.test.tsx    |   67 +-
 .../pages/alert-notice-template-fields.tsx         |  146 +-
 .../components/pages/alert-notice.chrome.test.ts   |    5 +-
 .../pages/alert-rule-quick-dialog.test.tsx         |   37 +
 .../components/pages/alert-rule-quick-dialog.tsx   |   16 +-
 .../pages/alert-setting-create-dialog.test.tsx     |  885 +++++-
 .../pages/alert-setting-create-dialog.tsx          | 1030 ++++++-
 .../pages/alert-setting-surface.test.tsx           |  387 +++
 .../components/pages/alert-setting-surface.tsx     |  521 +++-
 .../pages/alert-silence-authoring-fields.test.tsx  |   49 +
 .../pages/alert-silence-authoring-fields.tsx       |  138 +-
 .../pages/alert-silence-surface.test.tsx           |  119 +
 .../components/pages/alert-silence-surface.tsx     |  189 +-
 .../pages/bulletin-center-surface.test.tsx         |   14 +-
 .../components/pages/bulletin-center-surface.tsx   |    4 +-
 .../pages/bulletin-manage-dialog.test.tsx          |    9 +-
 .../components/pages/bulletin-manage-dialog.tsx    |    4 +
 .../pages/collector-manage-surface.test.tsx        |  174 +-
 .../components/pages/collector-manage-surface.tsx  |  389 ++-
 .../entity-definition-workspace-surface.test.tsx   |  858 +++++-
 .../pages/entity-definition-workspace-surface.tsx  | 1118 +++++++-
 .../pages/entity-detail-surface.test.tsx           |  953 ++++++-
 .../components/pages/entity-detail-surface.tsx     | 1262 ++++++--
 .../pages/entity-discovery-surface.test.tsx        |  198 +-
 .../components/pages/entity-discovery-surface.tsx  |  682 ++++-
 .../pages/entity-editor-surface.test.tsx           | 1655 ++++++++++-
 .../components/pages/entity-editor-surface.tsx     | 2763 ++++++++++++++++--
 .../pages/entity-import-surface.test.tsx           |   14 +-
 .../components/pages/entity-import-surface.tsx     |   23 +-
 .../components/pages/entity-list-surface.test.tsx  |  928 +++++-
 web-next/components/pages/entity-list-surface.tsx  |  873 +++++-
 .../pages/exception-center-surface.test.tsx        |    2 +
 .../components/pages/exception-center-surface.tsx  |    1 +
 .../components/pages/label-manage-surface.test.tsx |  201 +-
 web-next/components/pages/label-manage-surface.tsx |  443 ++-
 .../pages/login-form-submit-flow.test.tsx          |   58 +-
 web-next/components/pages/login-form.test.tsx      |   45 +-
 web-next/components/pages/login-form.tsx           |   53 +-
 .../pages/monitor-editor-surface.test.tsx          |  594 +++-
 .../components/pages/monitor-editor-surface.tsx    |  573 +++-
 .../components/pages/ops-surface-page.test.tsx     |  138 -
 web-next/components/pages/ops-surface-page.tsx     |  116 -
 web-next/components/pages/passport-shell.test.tsx  |   22 +-
 web-next/components/pages/passport-shell.tsx       |   17 +-
 .../pages/plugin-manage-surface.test.tsx           |  156 +-
 .../components/pages/plugin-manage-surface.tsx     |  478 +++-
 .../components/pages/public-status-shell.test.tsx  |    8 +
 web-next/components/pages/public-status-shell.tsx  |   52 +-
 .../pages/setting-define-surface.test.tsx          |  264 +-
 .../components/pages/setting-define-surface.tsx    |  119 +-
 .../pages/settings-surface-page.test.tsx           |   23 +-
 .../components/pages/settings-surface-page.tsx     |   83 +-
 .../pages/status-setting-surface.test.tsx          |  709 ++++-
 .../components/pages/status-setting-surface.tsx    | 1150 +++++++-
 .../settings/settings-console-shell.test.tsx       |   31 +-
 .../components/settings/settings-console-shell.tsx |   31 +-
 .../settings/settings-dialog-form.test.tsx         |   93 +-
 .../components/settings/settings-dialog-form.tsx   |  156 +-
 .../components/settings/settings-form.test.tsx     |   99 +-
 web-next/components/settings/settings-form.tsx     |  181 +-
 .../settings/settings-summary-list.test.tsx        |   18 +
 .../components/settings/settings-summary-list.tsx  |   40 +-
 web-next/components/shared-owner.chrome.test.ts    |    4 +-
 .../components/shell/app-frame.chrome.test.tsx     |   70 +-
 web-next/components/shell/app-frame.tsx            |  101 +-
 web-next/components/shell/app-sidebar.test.tsx     |    9 +-
 web-next/components/shell/app-sidebar.tsx          |   15 +-
 web-next/components/ui/hz-code-editor.test.tsx     |    5 +
 web-next/components/ui/hz-code-editor.tsx          |   19 +-
 web-next/components/ui/hz-confirm-dialog.test.tsx  |   24 +
 web-next/components/ui/hz-confirm-dialog.tsx       |    4 +-
 web-next/components/ui/search-row.test.tsx         |   64 +-
 web-next/components/ui/search-row.tsx              |   34 +-
 .../components/workbench/client-workbench.test.tsx |  232 +-
 web-next/components/workbench/client-workbench.tsx |   53 +-
 .../components/workbench/overlay-dialog.test.tsx   |   32 +
 web-next/components/workbench/overlay-dialog.tsx   |   30 +-
 web-next/components/workbench/toolbar.test.tsx     |    3 +-
 web-next/lib/actions-surface/model.test.ts         |   12 +-
 web-next/lib/actions-surface/model.ts              |   11 +-
 web-next/lib/actions-surface/view-model.test.ts    |   13 +
 web-next/lib/alert-group/query-state.test.ts       |   15 +
 web-next/lib/alert-group/query-state.ts            |    9 +-
 web-next/lib/alert-group/view-model.test.ts        |   24 +-
 web-next/lib/alert-group/view-model.ts             |   45 +-
 web-next/lib/alert-inhibit/query-state.test.ts     |   36 +
 web-next/lib/alert-inhibit/query-state.ts          |    9 +-
 web-next/lib/alert-inhibit/view-model.test.ts      |   80 +-
 web-next/lib/alert-inhibit/view-model.ts           |   52 +-
 web-next/lib/alert-manage/controller.test.ts       |   98 +-
 web-next/lib/alert-manage/controller.ts            |   23 +-
 web-next/lib/alert-manage/query-state.test.ts      |   54 +
 web-next/lib/alert-manage/query-state.ts           |   69 +-
 web-next/lib/alert-manage/view-model.test.ts       |  100 +
 web-next/lib/alert-manage/view-model.ts            |   67 +-
 web-next/lib/alert-notice/controller.test.ts       |   20 +-
 web-next/lib/alert-notice/controller.ts            |   26 +-
 web-next/lib/alert-notice/query-state.test.ts      |   15 +
 web-next/lib/alert-notice/query-state.ts           |    9 +-
 web-next/lib/alert-notice/view-model.test.ts       |   26 +-
 web-next/lib/alert-notice/view-model.ts            |  214 +-
 web-next/lib/alert-setting/view-model.test.ts      |    1 +
 web-next/lib/alert-setting/view-model.ts           |   16 +
 web-next/lib/alert-silence/query-state.test.ts     |   36 +
 web-next/lib/alert-silence/query-state.ts          |    9 +-
 web-next/lib/alert-silence/view-model.test.ts      |   78 +-
 web-next/lib/alert-silence/view-model.ts           |   52 +
 web-next/lib/api-client.test.ts                    |   23 +
 web-next/lib/api-client.ts                         |   50 +-
 web-next/lib/api-facade.test.ts                    |    4 +-
 web-next/lib/app-frame-state.test.ts               |    5 +-
 web-next/lib/app-frame-state.ts                    |   13 +-
 web-next/lib/bulletin-center/view-model.test.ts    |    6 +-
 web-next/lib/bulletin-center/view-model.ts         |   15 +-
 web-next/lib/client-workbench-state.test.ts        |   15 +-
 web-next/lib/client-workbench-state.ts             |   11 +-
 web-next/lib/collector-health-evidence.test.ts     |   17 +
 web-next/lib/collector-health-evidence.ts          |   13 +-
 web-next/lib/collector-manage/view-model.test.ts   |   12 +-
 web-next/lib/entity-definition/controller.test.ts  |   26 +-
 web-next/lib/entity-definition/controller.ts       |   17 +-
 web-next/lib/entity-detail/controller.test.ts      |   18 +-
 web-next/lib/entity-detail/controller.ts           |   27 +-
 web-next/lib/entity-detail/query-state.test.ts     |   22 +-
 web-next/lib/entity-detail/query-state.ts          |   12 +
 web-next/lib/entity-detail/view-model.test.ts      |  740 ++++-
 web-next/lib/entity-detail/view-model.ts           |  481 +++-
 web-next/lib/entity-discovery/controller.test.ts   |  157 +-
 web-next/lib/entity-discovery/controller.ts        |   88 +-
 web-next/lib/entity-discovery/search-state.test.ts |   45 +-
 web-next/lib/entity-discovery/search-state.ts      |  104 +
 web-next/lib/entity-discovery/view-model.test.ts   |  210 +-
 web-next/lib/entity-discovery/view-model.ts        |  184 +-
 web-next/lib/entity-editor/controller.test.ts      |  230 +-
 web-next/lib/entity-editor/controller.ts           |  144 +-
 web-next/lib/entity-editor/initial-state.test.ts   |   60 +-
 web-next/lib/entity-editor/initial-state.ts        |   64 +-
 web-next/lib/entity-editor/query-state.test.ts     |   20 +
 web-next/lib/entity-editor/query-state.ts          |   14 +-
 web-next/lib/entity-editor/view-model.test.ts      |   27 +
 web-next/lib/entity-editor/view-model.ts           |   56 +-
 web-next/lib/entity-health-affordance.test.ts      |   30 +-
 web-next/lib/entity-health-affordance.ts           |   15 +-
 web-next/lib/entity-import/controller.test.ts      |    4 +-
 web-next/lib/entity-import/controller.ts           |    6 +-
 web-next/lib/entity-import/view-model.test.ts      |  130 +-
 web-next/lib/entity-import/view-model.ts           |   43 +-
 web-next/lib/entity-manage/controller.test.ts      |   44 +
 web-next/lib/entity-manage/controller.ts           |   40 +-
 web-next/lib/entity-manage/query-state.test.ts     |  101 +-
 web-next/lib/entity-manage/query-state.ts          |   80 +-
 web-next/lib/entity-manage/view-model.test.ts      |  227 +-
 web-next/lib/entity-manage/view-model.ts           |   93 +-
 web-next/lib/hz-ops-visual.test.ts                 |   12 +-
 web-next/lib/hz-ops-visual.ts                      |    8 +-
 web-next/lib/i18n-runtime-messages.ts              | 3006 +++++++++++++++++++-
 web-next/lib/incidents-surface/controller.test.ts  |   51 +
 web-next/lib/incidents-surface/controller.ts       |   73 +-
 web-next/lib/incidents-surface/model.ts            |    3 +-
 web-next/lib/incidents-surface/view-model.test.ts  |    5 +
 web-next/lib/label-manage/query-state.test.ts      |    8 +-
 web-next/lib/label-manage/query-state.ts           |    9 +-
 web-next/lib/log-manage/view-model.test.ts         |   52 +
 web-next/lib/log-manage/view-model.ts              |   11 +-
 web-next/lib/monitor-api-facade.ts                 |    9 +-
 web-next/lib/monitor-detail/history-chart.ts       |    3 +-
 web-next/lib/monitor-editor/controller.test.ts     |   70 +-
 web-next/lib/monitor-editor/controller.ts          |  151 +-
 web-next/lib/monitor-manage/controller.test.ts     |   64 +
 web-next/lib/monitor-manage/controller.ts          |   31 +-
 web-next/lib/monitor-manage/query-state.test.ts    |  125 +
 web-next/lib/monitor-manage/query-state.ts         |   88 +-
 web-next/lib/monitor-manage/view-model.test.ts     |    2 +-
 web-next/lib/monitor-manage/view-model.ts          |    2 +-
 web-next/lib/nav.test.ts                           |    6 +-
 web-next/lib/object-store/view-model.test.ts       |   65 +
 web-next/lib/object-store/view-model.ts            |   40 +-
 web-next/lib/otlp-metrics/view-model.test.ts       |    7 +
 web-next/lib/otlp-metrics/view-model.ts            |   15 +-
 web-next/lib/parity/route-manifest.json            |   94 +-
 web-next/lib/parity/route-manifest.test.ts         |   94 +-
 web-next/lib/passport-login/view-model.test.ts     |   33 +-
 web-next/lib/passport-login/view-model.ts          |   45 +
 web-next/lib/session-bff.test.ts                   |   67 +-
 web-next/lib/session-bff.ts                        |  112 +-
 web-next/lib/setting-config/view-model.test.ts     |   79 +-
 web-next/lib/setting-config/view-model.ts          |   64 +-
 web-next/lib/setting-define/controller.test.ts     |    7 +
 web-next/lib/setting-define/controller.ts          |    5 +
 web-next/lib/setting-server/view-model.test.ts     |  100 +
 web-next/lib/setting-server/view-model.ts          |  143 +
 .../lib/setting-settings-layout/navigation.test.ts |   36 +
 web-next/lib/setting-settings-layout/navigation.ts |   33 +-
 web-next/lib/setting-status/query-state.ts         |   12 +
 web-next/lib/setting-status/view-model.test.ts     |   15 +-
 web-next/lib/setting-status/view-model.ts          |    6 +-
 web-next/lib/setting-token/controller.test.ts      |   22 +-
 web-next/lib/setting-token/controller.ts           |   30 +-
 web-next/lib/shell/sidebar.test.ts                 |   46 +
 web-next/lib/signal-dashboards.ts                  |    7 +-
 web-next/lib/signal-route-context.test.ts          |   41 +
 web-next/lib/signal-route-context.ts               |   27 +-
 web-next/lib/status-center/controller.test.ts      |   44 +
 web-next/lib/status-center/controller.ts           |   20 +-
 web-next/lib/status-center/view-model.test.ts      |   22 +
 web-next/lib/status-center/view-model.ts           |    4 +-
 web-next/lib/time-context.test.ts                  |   18 +
 web-next/lib/time-context.ts                       |    9 +-
 web-next/lib/topology-surface/controller.test.ts   |   24 +-
 web-next/lib/topology-surface/controller.ts        |   12 +-
 web-next/lib/topology-surface/query-state.test.ts  |   31 +
 web-next/lib/topology-surface/query-state.ts       |    4 +-
 web-next/lib/topology-surface/view-model.test.ts   |   62 +
 web-next/lib/topology-surface/view-model.ts        |   58 +-
 web-next/lib/trace-manage/query-state.test.ts      |   21 +-
 web-next/lib/trace-manage/query-state.ts           |   22 +-
 web-next/lib/trace-manage/view-model.test.ts       |    7 +
 web-next/lib/trace-manage/view-model.ts            |    6 +-
 web-next/lib/types.ts                              |   39 +
 web-next/lib/workbench-load-cache.test.ts          |   13 +-
 web-next/lib/workbench-load-cache.ts               |    4 +
 web-next/lucide-react-icon-node.d.ts               |    3 +
 web-next/package.json                              |    5 +-
 web-next/packages/hertzbeat-ui/src/index.test.tsx  |  275 +-
 web-next/packages/hertzbeat-ui/src/index.tsx       |  413 ++-
 .../packages/hertzbeat-ui/src/topology-g6.test.tsx |  171 +-
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx |   72 +-
 .../src/topology-metric-table.interaction.test.tsx |   63 +
 web-next/packages/hertzbeat-ui/src/topology.tsx    |    6 +-
 .../scripts/frontend-convergence-contract.test.ts  |   19 +-
 web-next/scripts/parity/harness-plan.test.ts       |   10 +-
 web-next/scripts/parity/owner-contracts.test.ts    |   15 +-
 .../scripts/product-design-validation-matrix.mjs   |  451 +++
 .../product-design-validation-matrix.test.ts       |  231 ++
 .../product-design-visual-governance.test.ts       |  200 ++
 .../scripts/release-route-browser-smoke.spec.ts    |    5 +
 web-next/scripts/release-shell-smoke.test.ts       |    3 +-
 web-next/scripts/route-matrix.mjs                  |    6 +
 web-next/scripts/standalone-server.mjs             |  141 +
 web-next/scripts/standalone-server.test.ts         |   54 +
 web-next/test/i18n-test-helper.ts                  |  712 ++++-
 466 files changed, 49264 insertions(+), 5422 deletions(-)

diff --git a/docs/product-design-user-validation-loop.md b/docs/product-design-user-validation-loop.md
new file mode 100644
index 0000000000..d42c949247
--- /dev/null
+++ b/docs/product-design-user-validation-loop.md
@@ -0,0 +1,214 @@
+# Product Design User Validation Loop
+
+This runbook makes Product Design part of the HertzBeat 2.0 user-layer
+validation loop. It is not a screenshot-only audit pass and it is not a visual
+polish layer. Use it to close product experience loops around real operator
+workflows, honest state feedback, data lifecycle clarity, and repeatable
+actions.
+
+## Product Context
+
+Start every Product Design workflow from saved context:
+
+- Product Design state: `$CODEX_HOME/state/plugins/product-design/user-context.md`
+- Local screenshot/reference assets:
+  `$CODEX_HOME/state/plugins/product-design/assets/`
+- Repo guidance: `AGENTS.md` and the latest active slice in `progress.md`
+- Frontend implementation: `web-next/app`, `web-next/components`,
+  `web-next/lib`, and `web-next/packages/hertzbeat-ui`
+- Theme and UI constraints: `web-next/app/globals.css`, shared
+  `@hertzbeat/ui` primitives, `lucide-react` icons, and runtime i18n catalogs
+
+Saved context should include the HertzBeat product position, operator audience,
+competitive references, design preferences, code paths, screenshots, design
+system constraints, and mainstream observability console expectations. Future
+audits, ideation, prototypes, and implementation work must prefer that context
+unless the user explicitly asks for a different direction.
+
+## Required Workflow Selection
+
+Use the smallest workflow that matches the problem.
+
+Small issue:
+
+```text
+audit -> small fix -> focused test -> Browser verification -> progress.md
+```
+
+Use this for a concrete user-visible problem in one flow, such as confusing
+error copy, an unclear button, an empty state that hides the data lifecycle, a
+broken cancel path, or a refresh/status mismatch.
+
+Systemic issue:
+
+```text
+user-context/get-context -> research -> audit -> ideate -> prototype -> design-qa -> small implementation slice -> progress.md
+```
+
+Use this when the issue exposes a broader product pattern, such as monitor
+authoring confusion, entity lifecycle ambiguity, alert rule authoring friction,
+or settings forms that repeatedly fail to explain required state and side
+effects.
+
+Do not skip `audit` or `research` before broad UI changes. Do not use Product
+Design to decorate an existing screen while leaving the real workflow,
+state feedback, error explanation, or data lifecycle unresolved.
+
+## Design Brief Gate
+
+Before any design judgment, experience redesign, prototype exploration, or UI
+implementation, confirm a brief through Product Design `get-context`.
+
+The brief must identify:
+
+- Target user and operating context.
+- Workflow problem being solved.
+- Existing route, screenshot, design system, or component reference.
+- Required interaction fidelity.
+- Backend/data lifecycle states that must stay honest.
+- Acceptance evidence expected before implementation is considered done.
+
+If the brief is missing or under-specified, stop and ask for only the missing
+context. Do not edit UI from an unconfirmed brief.
+
+## Research Gate
+
+For systemic problems, perform product and user research before designing a
+direction. Cover:
+
+- Operator mental model and repeated-use pressure.
+- Comparable observability platform patterns, especially Grafana, Datadog,
+  SigNoz, and OpenTelemetry ecosystem consoles where relevant.
+- State, error, loading, empty, and recovery language.
+- Help affordance density and progressive disclosure.
+- Safe authoring, save/cancel, detect/test, refresh, and status convergence.
+
+Prioritize monitor, entity, alert, and settings workflows because these are
+high-frequency operator surfaces.
+
+## Real Flow Audit
+
+Audits must use current product evidence:
+
+- Production frontend build.
+- Real HertzBeat backend, using H2/Greptime/warehouse data honestly.
+- In-app Browser screenshots for the current route state.
+- Step notes tied to every screenshot and finding.
+- Theme preflight before accepting screenshots: record `html[data-theme]`,
+  `body[data-theme]`, `--background`, `--card`, and `--ops-background`.
+  HertzBeat's default audit baseline is `dark-ops` (`--background: 220 11%
+  5%`, `--ops-background: #0b0c0e`). If an existing Browser origin has
+  persisted `light-ops`, do not use those screenshots as dark-baseline
+  evidence; use an isolated localhost port/origin or explicitly document that
+  the slice is auditing light theme.
+
+Audit at least the relevant parts of:
+
+- Login and guarded deep-link return.
+- Navigation and retained query/context parameters.
+- Create, edit, save, cancel, detect/test, refresh, and route revisit.
+- Status convergence without browser reload when the UI promises refresh.
+- Empty, loading, unavailable, failed, and recovered states.
+- Help question marks, button labels, destructive confirmations, and action
+  meaning.
+- Card hierarchy, table density, form grouping, text overflow, responsive
+  behavior, keyboard/focus semantics, and visible accessibility risks.
+
+Findings must cite screenshot names and step numbers. Do not evaluate from
+memory.
+
+## Ideation Gate
+
+When audit and research show a systemic UX problem, do not immediately rewrite
+product code. Generate two or three different directions first. Useful default
+directions for HertzBeat include:
+
+- Dense operations console: fastest scanning and repeat actions.
+- Diagnosis-first evidence chain: status, raw backend reason, metric evidence,
+  history availability, and recovery steps.
+- Guided authoring: lower misoperation risk for complex create/edit/test flows.
+
+Choose a direction before prototyping or implementing.
+
+## Prototype Gate
+
+For high-impact flows, build a local interactive prototype before production
+implementation. Prototype only the smallest slice needed to validate interaction
+and information architecture. Good candidates include:
+
+- Monitor create, detect, detail refresh, and history chart availability.
+- Alert rule authoring, preview/test, save/cancel, and template selection.
+- Entity create/edit/detail and definition import/edit handoff.
+- Settings forms with required fields, disabled save, cancel, and no-write
+  guarantees.
+
+The prototype is evidence for product decisions. It does not replace
+production code, tests, or Browser verification.
+
+## Implementation Gate
+
+Use Product Design `image-to-code` or production UI implementation only after
+there is a confirmed brief plus a concrete visual or interaction target:
+screenshot, selected visual direction, prototype, Figma frame, or mockup.
+
+Implementation must preserve:
+
+- Existing HertzBeat route/controller/view-model patterns.
+- Shared `@hertzbeat/ui` primitives and source-backed icons.
+- Dense operational layouts and stable dimensions.
+- Clear data lifecycle and backend-state honesty.
+- Runtime i18n rules and no hardcoded localized UI copy outside approved
+  locale resources.
+
+Do not introduce marketing-page composition, decorative chrome, fake zero
+states, fake healthy states, broad refactors, or untested UI rewrites.
+
+## Design QA Gate
+
+After implementation, run design QA against the target evidence:
+
+- Original audit screenshot(s).
+- Selected direction or prototype.
+- Actual production page rendered through the in-app Browser.
+- Focused automated tests and the smallest relevant Browser proof.
+
+Check layout, hierarchy, density, text overflow, responsive behavior, control
+states, focus/keyboard semantics, empty/error/loading states, and visible
+accessibility risks. P0, P1, and P2 design issues must be fixed before the
+slice is treated as passing.
+
+## Artifact And Progress Policy
+
+Persist Product Design evidence locally. Use clear names that can be understood
+without opening the file.
+
+Recommended local artifact layout:
+
+```text
+.tmp/product-design/
+  <slice-id>/
+    screenshots/
+    audit.md
+    research.md
+    directions.md
+    prototype-notes.md
+    design-qa.md
+```
+
+Do not commit `.tmp/`, screenshots, reports, runtime logs, local proof data, or
+generated artifacts unless maintainers explicitly ask. The active slice in
+`progress.md` must record:
+
+- Saved Product Design context used.
+- Research sources or research scope.
+- Audit route, backend, screenshots, and steps.
+- Theme preflight evidence and whether the screenshot baseline is `dark-ops`
+  or an intentional alternate theme.
+- Findings by severity and user impact.
+- Selected direction and prototype target, when applicable.
+- Implementation files changed.
+- Focused tests and Browser verification.
+- Remaining risks and next slice.
+
+The release gate is not complete until Product Design evidence supports the
+actual operator workflow, not just a single screenshot or isolated component.
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 a01983b1d6..6c056841e7 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
@@ -87,7 +87,7 @@ public class AlertDefineServiceImpl implements AlertDefineService {
     @Resource
     private LabelDao labelDao;
 
-    @Resource
+    @Autowired(required = false)
     private List<HistoryDataReader> historyDataReaders = Collections.emptyList();
 
     private final DataSourceService dataSourceService;
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntityOpsSummaryInfo.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntityOpsSummaryInfo.java
index 2b4b2b314d..ce38625364 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntityOpsSummaryInfo.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntityOpsSummaryInfo.java
@@ -17,7 +17,6 @@
 
 package org.apache.hertzbeat.common.observability.dto.entity;
 
-import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
@@ -25,7 +24,6 @@ import lombok.NoArgsConstructor;
  * Operational readiness summary for entity workspace.
  */
 @Data
-@AllArgsConstructor
 @NoArgsConstructor
 public class EntityOpsSummaryInfo {
 
@@ -40,4 +38,32 @@ public class EntityOpsSummaryInfo {
     private boolean statusReady;
 
     private int readinessScore;
+
+    private long relationCount;
+
+    public EntityOpsSummaryInfo(boolean ownerReady,
+                                boolean runbookReady,
+                                boolean relationReady,
+                                boolean telemetryReady,
+                                boolean statusReady,
+                                int readinessScore,
+                                long relationCount) {
+        this.ownerReady = ownerReady;
+        this.runbookReady = runbookReady;
+        this.relationReady = relationReady;
+        this.telemetryReady = telemetryReady;
+        this.statusReady = statusReady;
+        this.readinessScore = readinessScore;
+        this.relationCount = relationCount;
+    }
+
+    public EntityOpsSummaryInfo(boolean ownerReady,
+                                boolean runbookReady,
+                                boolean relationReady,
+                                boolean telemetryReady,
+                                boolean statusReady,
+                                int readinessScore) {
+        this(ownerReady, runbookReady, relationReady, telemetryReady, statusReady,
+                readinessScore, relationReady ? 1L : 0L);
+    }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/EntityController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/EntityController.java
index 01b10a04fb..938bf76b34 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/EntityController.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/EntityController.java
@@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import java.util.List;
+import java.util.Map;
 import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionRequest;
 import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionActivityInfo;
 import org.apache.hertzbeat.manager.pojo.dto.EntityCatalogSuggestionsInfo;
@@ -90,6 +91,15 @@ public class EntityController {
         return ResponseEntity.ok(Message.success(entityDto));
     }
 
+    @PostMapping("/{id:\\d+}/definition/parse")
+    @Operation(summary = "Parse entity definition for an entity", description = "Parse entity definition content while allowing the current entity identity")
+    public ResponseEntity<Message<EntityDto>> parseEntityDefinitionForEntity(
+            @Parameter(description = "Entity ID", example = "87584674384") @PathVariable("id") long id,
+            @Valid @RequestBody EntityDefinitionRequest definitionRequest) {
+        EntityDto entityDto = observeEntityService.parseEntityDefinition(definitionRequest, id);
+        return ResponseEntity.ok(Message.success(entityDto));
+    }
+
     @PostMapping("/definition/bundle/parse")
     @Operation(summary = "Parse entity definition bundle", description = "Parse entity definition bundle content to entity dto list")
     public ResponseEntity<Message<List<EntityDto>>> parseEntityDefinitionBundle(@Valid @RequestBody EntityDefinitionRequest definitionRequest) {
@@ -332,4 +342,12 @@ public class EntityController {
         List<EntityMonitorBindingCandidate> candidates = observeEntityService.getMonitorBindingCandidates(monitorId);
         return ResponseEntity.ok(Message.success(candidates));
     }
+
+    @GetMapping("/monitor/candidates")
+    @Operation(summary = "Recommend entities for monitors", description = "Recommend entities for multiple monitors")
+    public ResponseEntity<Message<Map<Long, List<EntityMonitorBindingCandidate>>>> getMonitorBindingCandidates(
+            @Parameter(description = "Monitor IDs", example = "87584674384") @RequestParam List<Long> ids) {
+        Map<Long, List<EntityMonitorBindingCandidate>> candidates = observeEntityService.getMonitorBindingCandidates(ids);
+        return ResponseEntity.ok(Message.success(candidates));
+    }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java
index 7ad5486948..c014c4d0bf 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java
@@ -42,5 +42,10 @@ public interface BulletinDao extends JpaRepository<Bulletin, Long>, JpaSpecifica
      */
     int countByName(String name);
 
+    /**
+     * Count other Bulletins by name
+     */
+    int countByNameAndIdNot(String name, Long id);
+
 
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityIdentityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityIdentityDao.java
index 5216948ca4..96f0f228f0 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityIdentityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityIdentityDao.java
@@ -17,6 +17,7 @@
 
 package org.apache.hertzbeat.manager.dao;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
@@ -41,6 +42,9 @@ public interface EntityIdentityDao extends JpaRepository<EntityIdentity, Long>,
     @Query("SELECT COUNT(DISTINCT identity.entityId) FROM EntityIdentity identity WHERE identity.identityKey IN :identityKeys")
     long countDistinctEntityIdsByIdentityKeyIn(@Param("identityKeys") Set<String> identityKeys);
 
+    @Query("SELECT identity.entityId, COUNT(identity) FROM EntityIdentity identity WHERE identity.entityId IN :entityIds GROUP BY identity.entityId")
+    List<Object[]> countByEntityIdInGroupByEntityId(@Param("entityIds") Collection<Long> entityIds);
+
     @Modifying(clearAutomatically = true, flushAutomatically = true)
     @Query("DELETE FROM EntityIdentity identity WHERE identity.entityId = :entityId")
     void deleteAllByEntityId(@Param("entityId") Long entityId);
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityMonitorBindDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityMonitorBindDao.java
index cd8f8b8ba9..bd548e8540 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityMonitorBindDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityMonitorBindDao.java
@@ -17,6 +17,7 @@
 
 package org.apache.hertzbeat.manager.dao;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
@@ -33,6 +34,8 @@ public interface EntityMonitorBindDao extends JpaRepository<EntityMonitorBind, L
 
     List<EntityMonitorBind> findAllByEntityIdOrderByIdAsc(Long entityId);
 
+    List<EntityMonitorBind> findAllByEntityIdInOrderByEntityIdAscIdAsc(Collection<Long> entityIds);
+
     List<EntityMonitorBind> findAllByMonitorId(Long monitorId);
 
     @Modifying(clearAutomatically = true, flushAutomatically = true)
@@ -46,4 +49,7 @@ public interface EntityMonitorBindDao extends JpaRepository<EntityMonitorBind, L
     void deleteAllByMonitorIdIn(Set<Long> monitorIds);
 
     long countByEntityId(Long entityId);
+
+    @Query("SELECT bind.entityId, COUNT(bind) FROM EntityMonitorBind bind WHERE bind.entityId IN :entityIds GROUP BY bind.entityId")
+    List<Object[]> countByEntityIdInGroupByEntityId(@Param("entityIds") Collection<Long> entityIds);
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityRelationDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityRelationDao.java
index 4edf781aa9..8d5c15a033 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityRelationDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityRelationDao.java
@@ -17,8 +17,10 @@
 
 package org.apache.hertzbeat.manager.dao;
 
+import java.util.Collection;
 import java.util.List;
 import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.data.jpa.repository.Modifying;
@@ -32,6 +34,8 @@ public interface EntityRelationDao extends JpaRepository<EntityRelation, Long>,
 
     List<EntityRelation> findBySourceEntityIdOrTargetEntityId(Long sourceEntityId, Long targetEntityId);
 
+    List<EntityRelation> findBySourceEntityIdOrTargetEntityId(Long sourceEntityId, Long targetEntityId, Pageable pageable);
+
     @Modifying(clearAutomatically = true, flushAutomatically = true)
     @Query("DELETE FROM EntityRelation relation WHERE relation.sourceEntityId = :sourceEntityId")
     void deleteAllBySourceEntityId(@Param("sourceEntityId") Long sourceEntityId);
@@ -42,4 +46,16 @@ public interface EntityRelationDao extends JpaRepository<EntityRelation, Long>,
                                                    @Param("targetEntityId") Long targetEntityId);
 
     long countBySourceEntityIdOrTargetEntityId(Long sourceEntityId, Long targetEntityId);
+
+    @Query("SELECT relation.sourceEntityId, COUNT(relation) FROM EntityRelation relation WHERE relation.sourceEntityId IN :entityIds GROUP BY relation.sourceEntityId")
+    List<Object[]> countBySourceEntityIdInGroupBySourceEntityId(@Param("entityIds") Collection<Long> entityIds);
+
+    @Query("""
+            SELECT relation.targetEntityId, COUNT(relation)
+            FROM EntityRelation relation
+            WHERE relation.targetEntityId IN :entityIds
+              AND (relation.sourceEntityId IS NULL OR relation.sourceEntityId <> relation.targetEntityId)
+            GROUP BY relation.targetEntityId
+            """)
+    List<Object[]> countByTargetEntityIdInGroupByTargetEntityId(@Param("entityIds") Collection<Long> entityIds);
 }
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 9bd509760b..33c0856c2b 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
@@ -34,11 +34,15 @@ public interface ObserveEntityDao extends JpaRepository<ObserveEntity, Long>, Jp
 
     Optional<ObserveEntity> findFirstByTypeAndName(String type, String name);
 
+    Optional<ObserveEntity> findFirstByName(String name);
+
     Optional<ObserveEntity> findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
             String workspaceId, String type, String namespace, String name);
 
     Optional<ObserveEntity> findFirstByWorkspaceIdAndTypeAndName(String workspaceId, String type, String name);
 
+    Optional<ObserveEntity> findFirstByWorkspaceIdAndName(String workspaceId, 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/service/ObserveEntityService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ObserveEntityService.java
index b5b32c89f1..79ddb88715 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ObserveEntityService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ObserveEntityService.java
@@ -18,6 +18,7 @@
 package org.apache.hertzbeat.manager.service;
 
 import java.util.List;
+import java.util.Map;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.observability.dto.entity.MonitorInfo;
 import org.apache.hertzbeat.manager.pojo.dto.EntityCatalogSuggestionsInfo;
@@ -102,4 +103,6 @@ public interface ObserveEntityService {
                                         String sort, String order, int pageIndex, int pageSize);
 
     List<EntityMonitorBindingCandidate> getMonitorBindingCandidates(long monitorId);
+
+    Map<Long, List<EntityMonitorBindingCandidate>> getMonitorBindingCandidates(List<Long> monitorIds);
 }
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
index b513097539..0e65ce14c2 100644
--- 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
@@ -20,8 +20,12 @@ 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.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -171,33 +175,71 @@ public class EntityCatalogQueryService {
         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));
+        for (String normalizedSearch : normalizeSearchTextVariants(search)) {
+            String lower = "%" + normalizedSearch + "%";
+            orList.add(criteriaBuilder.equal(criteriaBuilder.lower(root.get("name")), normalizedSearch));
+            orList.add(criteriaBuilder.equal(criteriaBuilder.lower(root.get("displayName")), normalizedSearch));
+            orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), lower));
+            orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("displayName")), lower));
+            List<Predicate> nameTokenPredicates = new ArrayList<>();
+            List<Predicate> displayNameTokenPredicates = new ArrayList<>();
+            for (String token : normalizedSearch.split("[^a-z0-9]+")) {
+                if (token.length() < 2 || token.equals(normalizedSearch)) {
+                    continue;
+                }
+                String tokenLike = "%" + token + "%";
+                nameTokenPredicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), tokenLike));
+                displayNameTokenPredicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("displayName")), tokenLike));
+            }
+            if (!nameTokenPredicates.isEmpty()) {
+                orList.add(criteriaBuilder.or(
+                        criteriaBuilder.and(nameTokenPredicates.toArray(new Predicate[0])),
+                        criteriaBuilder.and(displayNameTokenPredicates.toArray(new Predicate[0]))));
+            }
+            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;
     }
+
+    private List<String> normalizeSearchTextVariants(String search) {
+        String trimmedSearch = search.trim();
+        String normalizedSearch;
+        try {
+            normalizedSearch = URLDecoder.decode(trimmedSearch, StandardCharsets.UTF_8).toLowerCase();
+        } catch (IllegalArgumentException ignored) {
+            normalizedSearch = trimmedSearch.toLowerCase();
+        }
+        Set<String> variants = new LinkedHashSet<>();
+        variants.add(normalizedSearch);
+        if (normalizedSearch.contains("localhost")) {
+            variants.add(normalizedSearch.replace("localhost", "127.0.0.1"));
+        }
+        if (normalizedSearch.contains("127.0.0.1")) {
+            variants.add(normalizedSearch.replace("127.0.0.1", "localhost"));
+        }
+        return new ArrayList<>(variants);
+    }
 }
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
index 378c349829..7f355efbbf 100644
--- 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
@@ -38,9 +38,9 @@ public class EntityDefinitionDocumentParserService {
     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);
+        DefinitionPayload payload = extractDefinitionPayload(definitionRequest.getContent(), definitionRequest.getFormat());
+        String format = normalizeDefinitionFormat(payload.format(), payload.content());
+        return parseDefinitionDocuments(payload.content(), format);
     }
 
     private List<Map<String, Object>> parseDefinitionDocuments(String payload, String format) {
@@ -74,35 +74,116 @@ public class EntityDefinitionDocumentParserService {
         return List.of(toDefinitionRecord(value));
     }
 
-    private String extractDefinitionPayload(String content, String format) {
+    private DefinitionPayload 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("\\\\", "\\");
+            return new DefinitionPayload(trimmed, format);
+        }
+        String payload = extractCurlDataPayload(trimmed);
+        if (payload != null) {
+            return unwrapCurlRequestEnvelope(payload, format);
+        }
+        return unwrapCurlRequestEnvelope(trimmed, format);
+    }
+
+    private String extractCurlDataPayload(String curlCommand) {
+        for (String flag : List.of("--data-urlencode", "--data-binary", "--data-raw", "--data", "-d")) {
+            int searchFrom = 0;
+            int flagIndex;
+            while ((flagIndex = curlCommand.indexOf(flag, searchFrom)) >= 0) {
+                int valueStart = flagIndex + flag.length();
+                if (valueStart < curlCommand.length() && curlCommand.charAt(valueStart) == '=') {
+                    valueStart++;
+                } else if (valueStart < curlCommand.length() && !Character.isWhitespace(curlCommand.charAt(valueStart))) {
+                    searchFrom = valueStart;
+                    continue;
+                }
+                while (valueStart < curlCommand.length() && Character.isWhitespace(curlCommand.charAt(valueStart))) {
+                    valueStart++;
+                }
+                if (valueStart >= curlCommand.length()) {
+                    return null;
+                }
+                return readCurlDataValue(curlCommand, valueStart);
             }
         }
-        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 null;
+    }
+
+    private String readCurlDataValue(String curlCommand, int valueStart) {
+        char first = curlCommand.charAt(valueStart);
+        if (first == '\'') {
+            return readSingleQuotedCurlValue(curlCommand, valueStart + 1)
+                    .replace("'\\\\''", "'")
+                    .replace("\\\\", "\\");
+        }
+        if (first == '"') {
+            return readDoubleQuotedCurlValue(curlCommand, valueStart + 1)
+                    .replace("\\\"", "\"")
+                    .replace("\\\\", "\\");
+        }
+        int valueEnd = valueStart;
+        while (valueEnd < curlCommand.length() && !Character.isWhitespace(curlCommand.charAt(valueEnd))) {
+            valueEnd++;
+        }
+        return curlCommand.substring(valueStart, valueEnd);
+    }
+
+    private String readSingleQuotedCurlValue(String curlCommand, int payloadStart) {
+        int index = payloadStart;
+        while (index < curlCommand.length()) {
+            if (curlCommand.charAt(index) == '\'') {
+                if (index + 3 < curlCommand.length()
+                        && curlCommand.charAt(index + 1) == '\\'
+                        && curlCommand.charAt(index + 2) == '\''
+                        && curlCommand.charAt(index + 3) == '\'') {
+                    index += 4;
+                    continue;
+                }
+                return curlCommand.substring(payloadStart, index);
+            }
+            index++;
+        }
+        return curlCommand.substring(payloadStart);
+    }
+
+    private String readDoubleQuotedCurlValue(String curlCommand, int payloadStart) {
+        int index = payloadStart;
+        boolean escaped = false;
+        while (index < curlCommand.length()) {
+            char current = curlCommand.charAt(index);
+            if (current == '"' && !escaped) {
+                return curlCommand.substring(payloadStart, index);
+            }
+            escaped = current == '\\' && !escaped;
+            if (current != '\\') {
+                escaped = false;
             }
+            index++;
         }
-        return trimmed;
+        return curlCommand.substring(payloadStart);
+    }
+
+    private DefinitionPayload unwrapCurlRequestEnvelope(String payload, String format) {
+        if (!JsonUtil.isJsonStr(payload)) {
+            return new DefinitionPayload(payload, format);
+        }
+        Object parsed = JsonUtil.fromJson(payload, Object.class);
+        if (!(parsed instanceof Map<?, ?> rawMap)) {
+            return new DefinitionPayload(payload, format);
+        }
+        Object content = rawMap.get("content");
+        if (!(content instanceof String contentText) || !StringUtils.hasText(contentText)) {
+            return new DefinitionPayload(payload, format);
+        }
+        Object envelopeFormat = rawMap.get("format");
+        return new DefinitionPayload(
+                contentText.trim(),
+                defaultText(envelopeFormat == null ? null : String.valueOf(envelopeFormat), format)
+        );
     }
 
     private String normalizeDefinitionFormat(String format, String payload) {
@@ -141,4 +222,7 @@ public class EntityDefinitionDocumentParserService {
         }
         return "";
     }
+
+    private record DefinitionPayload(String content, String format) {
+    }
 }
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
index 1dfeb23811..5d17fb9d02 100644
--- 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
@@ -17,11 +17,18 @@
 
 package org.apache.hertzbeat.manager.service.entity;
 
+import java.util.ArrayList;
+import java.util.HashSet;
 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.apache.hertzbeat.manager.pojo.dto.EntityDefinitionRequest;
 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 DTO drafts from submitted definition documents.
@@ -32,13 +39,25 @@ public class EntityDefinitionDraftService {
     private final EntityDefinitionDocumentParserService entityDefinitionDocumentParserService;
     private final EntityDefinitionNormalizationService entityDefinitionNormalizationService;
     private final EntityDefinitionMappingService entityDefinitionMappingService;
+    private final EntityIdentityDao entityIdentityDao;
+    private final EntityIdentityResolutionService entityIdentityResolutionService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+    private final EntityMonitorBindQueryService entityMonitorBindQueryService;
 
     public EntityDefinitionDraftService(EntityDefinitionDocumentParserService entityDefinitionDocumentParserService,
                                         EntityDefinitionNormalizationService entityDefinitionNormalizationService,
-                                        EntityDefinitionMappingService entityDefinitionMappingService) {
+                                        EntityDefinitionMappingService entityDefinitionMappingService,
+                                        EntityIdentityDao entityIdentityDao,
+                                        EntityIdentityResolutionService entityIdentityResolutionService,
+                                        EntityWorkspaceAccessService entityWorkspaceAccessService,
+                                        EntityMonitorBindQueryService entityMonitorBindQueryService) {
         this.entityDefinitionDocumentParserService = entityDefinitionDocumentParserService;
         this.entityDefinitionNormalizationService = entityDefinitionNormalizationService;
         this.entityDefinitionMappingService = entityDefinitionMappingService;
+        this.entityIdentityDao = entityIdentityDao;
+        this.entityIdentityResolutionService = entityIdentityResolutionService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+        this.entityMonitorBindQueryService = entityMonitorBindQueryService;
     }
 
     public EntityDto parseEntityDefinition(EntityDefinitionRequest definitionRequest, Long entityId) {
@@ -79,11 +98,117 @@ public class EntityDefinitionDraftService {
                                                        Long entityId,
                                                        String requestWorkspaceId,
                                                        boolean resolveRequestWorkspaceAtMappingBoundary) {
-        return entityDefinitionDocumentParserService.parseDefinitionRecords(definitionRequest).stream()
+        List<EntityDto> entityDtos = entityDefinitionDocumentParserService.parseDefinitionRecords(definitionRequest).stream()
                 .map(entityDefinitionNormalizationService::normalizeDefinition)
                 .map(definition -> resolveRequestWorkspaceAtMappingBoundary
                         ? entityDefinitionMappingService.toEntityDto(definition, entityId)
                         : entityDefinitionMappingService.toEntityDto(definition, entityId, requestWorkspaceId))
                 .toList();
+        rejectPreviewPrimaryIdentityCollisions(entityDtos, entityId);
+        rejectPreviewMonitorBindCollisions(entityDtos, entityId);
+        return entityDtos;
+    }
+
+    private void rejectPreviewMonitorBindCollisions(List<EntityDto> entityDtos, Long currentEntityId) {
+        for (EntityDto entityDto : entityDtos) {
+            if (entityDto == null || CollectionUtils.isEmpty(entityDto.getMonitorBinds())) {
+                continue;
+            }
+            for (org.apache.hertzbeat.common.entity.manager.EntityMonitorBind monitorBind : entityDto.getMonitorBinds()) {
+                if (monitorBind == null || monitorBind.getMonitorId() == null) {
+                    continue;
+                }
+                for (org.apache.hertzbeat.common.entity.manager.EntityMonitorBind existingBind
+                        : entityMonitorBindQueryService.findMonitorBindsByMonitorId(monitorBind.getMonitorId())) {
+                    if (existingBind == null || existingBind.getEntityId() == null
+                            || existingBind.getEntityId().equals(currentEntityId)) {
+                        continue;
+                    }
+                    throw new IllegalArgumentException("Monitor already bound to another entity: "
+                            + monitorBind.getMonitorId() + ".");
+                }
+            }
+        }
+    }
+
+    private void rejectPreviewPrimaryIdentityCollisions(List<EntityDto> entityDtos, Long currentEntityId) {
+        List<PreviewPrimaryIdentity> primaryIdentities = collectPreviewPrimaryIdentities(entityDtos);
+        if (primaryIdentities.isEmpty()) {
+            return;
+        }
+
+        Set<String> identityKeys = new HashSet<>();
+        Set<String> normalizedValues = new HashSet<>();
+        for (PreviewPrimaryIdentity identity : primaryIdentities) {
+            identityKeys.add(identity.identityKey());
+            normalizedValues.add(identity.normalizedValue());
+        }
+
+        List<EntityIdentity> matchingIdentities =
+                entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(identityKeys, normalizedValues);
+        if (CollectionUtils.isEmpty(matchingIdentities)) {
+            return;
+        }
+
+        Set<Long> matchingEntityIds = new HashSet<>();
+        for (EntityIdentity matchingIdentity : matchingIdentities) {
+            Long matchingEntityId = matchingIdentity.getEntityId();
+            if (matchingEntityId != null && !matchingEntityId.equals(currentEntityId)) {
+                matchingEntityIds.add(matchingEntityId);
+            }
+        }
+        if (matchingEntityIds.isEmpty()) {
+            return;
+        }
+
+        Set<Long> accessibleEntityIds = new HashSet<>(entityWorkspaceAccessService
+                .findAccessibleEntitiesByIdsForRequestWorkspace(matchingEntityIds)
+                .stream()
+                .map(ObserveEntity::getId)
+                .toList());
+        if (accessibleEntityIds.isEmpty()) {
+            return;
+        }
+
+        for (PreviewPrimaryIdentity primaryIdentity : primaryIdentities) {
+            for (EntityIdentity matchingIdentity : matchingIdentities) {
+                if (!accessibleEntityIds.contains(matchingIdentity.getEntityId())) {
+                    continue;
+                }
+                if (matchingIdentity.isPrimaryIdentity()
+                        && primaryIdentity.identityKey().equals(matchingIdentity.getIdentityKey())
+                        && primaryIdentity.normalizedValue().equals(matchingIdentity.getNormalizedValue())) {
+                    throw new IllegalArgumentException("Entity primary identity already exists: "
+                            + primaryIdentity.identityKey() + "=" + primaryIdentity.identityValue() + ".");
+                }
+            }
+        }
+    }
+
+    private List<PreviewPrimaryIdentity> collectPreviewPrimaryIdentities(List<EntityDto> entityDtos) {
+        List<PreviewPrimaryIdentity> identities = new ArrayList<>();
+        for (EntityDto entityDto : entityDtos) {
+            if (entityDto == null || CollectionUtils.isEmpty(entityDto.getIdentities())) {
+                continue;
+            }
+            for (EntityIdentity identity : entityDto.getIdentities()) {
+                if (identity == null
+                        || !identity.isPrimaryIdentity()
+                        || !StringUtils.hasText(identity.getIdentityKey())
+                        || !StringUtils.hasText(identity.getIdentityValue())) {
+                    continue;
+                }
+                String identityKey = identity.getIdentityKey().trim();
+                String identityValue = identity.getIdentityValue().trim();
+                String normalizedValue = entityIdentityResolutionService.normalizeIdentityValue(identityKey, identityValue);
+                if (StringUtils.hasText(normalizedValue)) {
+                    identities.add(new PreviewPrimaryIdentity(identityKey, identityValue, normalizedValue));
+                }
+            }
+        }
+        return identities;
+    }
+
+    private record PreviewPrimaryIdentity(String identityKey, String identityValue, String normalizedValue) {
     }
 }
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
index 5b4a8aefe0..3b51ed0975 100644
--- 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
@@ -151,10 +151,13 @@ public class EntityDefinitionRelationNormalizationService {
     }
 
     private String relationSignature(EntityDefinition.Relation relation) {
+        String targetReference = defaultText(
+                relation.getTargetRef(),
+                buildEntityReference(relation.getTargetEntityId()),
+                relation.getTargetEntityId() == null ? "" : String.valueOf(relation.getTargetEntityId()));
         return String.join("|",
                 defaultText(relation.getRelationType(), "depends_on"),
-                relation.getTargetEntityId() == null ? "" : String.valueOf(relation.getTargetEntityId()),
-                relation.getTargetRef() == null ? "" : relation.getTargetRef());
+                targetReference.trim().toLowerCase(java.util.Locale.ROOT));
     }
 
     private String buildEntityReference(Long entityId) {
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
index a86d6859e5..41d8604c8b 100644
--- 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
@@ -47,19 +47,29 @@ public class EntityDefinitionTypeResolverService {
     }
 
     public String resolveDefinitionEntityType(Map<String, Object> root, Map<String, Object> specMap) {
-        String normalizedKind = normalizeSupportedEntityType(asText(root.get("kind")));
+        String rawKind = asText(root.get("kind"));
+        String normalizedKind = normalizeSupportedEntityType(rawKind);
         if (StringUtils.hasText(normalizedKind)) {
             return normalizedKind;
         }
-        String normalizedEntityType = normalizeSupportedEntityType(defaultText(
+        if (isUnsupportedExplicitKind(rawKind)) {
+            throw new IllegalArgumentException("Unsupported entity definition kind: " + rawKind + ".");
+        }
+
+        String rawEntityType = defaultText(
                 asText(specMap.get("entityType")),
                 asText(specMap.get("entity_type")),
                 asText(root.get("entityType")),
                 asText(root.get("entity_type"))
-        ));
+        );
+        String normalizedEntityType = normalizeSupportedEntityType(rawEntityType);
         if (StringUtils.hasText(normalizedEntityType)) {
             return normalizedEntityType;
         }
+        if (StringUtils.hasText(rawEntityType)) {
+            throw new IllegalArgumentException("Unsupported entity definition entity type: " + rawEntityType + ".");
+        }
+
         return defaultText(
                 normalizeSupportedEntityType(asText(specMap.get("type"))),
                 normalizeSupportedEntityType(asText(root.get("type"))),
@@ -68,6 +78,10 @@ public class EntityDefinitionTypeResolverService {
         );
     }
 
+    private boolean isUnsupportedExplicitKind(String rawKind) {
+        return StringUtils.hasText(rawKind) && !LEGACY_ENTITY_DEFINITION_KIND.equalsIgnoreCase(rawKind.trim());
+    }
+
     public String resolveDefinitionSubtype(Map<String, Object> root, Map<String, Object> specMap, String entityType) {
         String explicitSubtype = defaultText(
                 asText(specMap.get("subtype")),
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 c790424de4..f198c1121a 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
@@ -61,6 +61,9 @@ import org.springframework.util.CollectionUtils;
 @Service
 public class EntityDetailObservabilityReadModelService {
 
+    static final int ENTITY_DETAIL_RELATION_PREVIEW_LIMIT = 50;
+    static final int ENTITY_DETAIL_BOUND_MONITOR_PREVIEW_LIMIT = 50;
+
     private final EntityDetailReadModelService entityDetailReadModelService;
     private final EntityStatusRefreshService entityStatusRefreshService;
     private final EntityObservabilityGateway entityObservabilityGateway;
@@ -86,24 +89,26 @@ public class EntityDetailObservabilityReadModelService {
     }
 
     public EntityDetailDto buildEntityDetail(long entityId) {
-        EntityDto entityDto = entityDetailReadModelService.loadEntityDto(entityId);
+        EntityDto entityDto = entityDetailReadModelService.loadEntityDto(
+                entityId, ENTITY_DETAIL_RELATION_PREVIEW_LIMIT);
         if (entityDto == null) {
             return null;
         }
-        return buildEntityDetail(entityDto);
+        return buildEntityDetail(entityDto, null, true, entityDetailReadModelService.countEntityRelations(entityId));
     }
 
     public EntityDetailDto buildEntityDetail(EntityDto entityDto) {
-        return buildEntityDetail(entityDto, null, true);
+        return buildEntityDetail(entityDto, null, true, null);
     }
 
     public EntityDetailDto buildEntityDetail(EntityDto entityDto, String requestWorkspaceId) {
-        return buildEntityDetail(entityDto, requestWorkspaceId, false);
+        return buildEntityDetail(entityDto, requestWorkspaceId, false, null);
     }
 
     private EntityDetailDto buildEntityDetail(EntityDto entityDto,
                                               String requestWorkspaceId,
-                                              boolean resolveRequestWorkspaceAtEvidenceBoundaries) {
+                                              boolean resolveRequestWorkspaceAtEvidenceBoundaries,
+                                              Long relationCountOverride) {
         if (entityDto == null || entityDto.getEntity() == null) {
             return null;
         }
@@ -124,7 +129,10 @@ public class EntityDetailObservabilityReadModelService {
                 entityDto.getEntity(), entityDto.getIdentities(),
                 entityDto.getEntityInfo() == null ? null : entityDto.getEntityInfo().getHertzbeat()
         );
-        List<MonitorInfo> boundMonitors = monitors.stream().map(MonitorInfo::fromEntity).toList();
+        List<MonitorInfo> boundMonitors = monitors.stream()
+                .limit(ENTITY_DETAIL_BOUND_MONITOR_PREVIEW_LIMIT)
+                .map(MonitorInfo::fromEntity)
+                .toList();
         List<EntityLogQueryHint> logQueryHints =
                 entityObservabilityGateway.buildEntityLogQueryHints(entityDto.getIdentities(), monitors);
         EntityEvidenceSummaryInfo evidenceSummary = entityObservabilityGateway.buildEntityEvidenceSummary(
@@ -150,7 +158,9 @@ public class EntityDetailObservabilityReadModelService {
         List<TraceEvidence> traceEvidence = observabilityDetail.getTraceEvidence();
         EntityUnifiedEvidenceSummary unifiedEvidenceSummary = observabilityDetail.getUnifiedEvidenceSummary();
         EntityTriageRecommendation triageRecommendation = observabilityDetail.getTriageRecommendation();
-        long relationCount = CollectionUtils.isEmpty(entityDto.getRelations()) ? 0 : entityDto.getRelations().size();
+        long relationCount = relationCountOverride == null
+                ? CollectionUtils.isEmpty(entityDto.getRelations()) ? 0 : entityDto.getRelations().size()
+                : relationCountOverride;
         EntityOpsSummaryInfo opsSummary = entityObservabilityGateway.buildEntityOpsSummary(
                 entityDto.getEntity(), relationCount, evidenceSummary);
         List<EntityNextActionInfo> nextActions =
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
index 8796bbdc28..df7de3dd75 100644
--- 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
@@ -47,12 +47,34 @@ public class EntityDetailReadModelService {
         return buildEntityDto(entityId, entityWorkspaceAccessService.findAccessibleEntityForRequestWorkspace(entityId));
     }
 
+    public EntityDto loadEntityDto(long entityId, int relationPreviewLimit) {
+        return buildEntityDto(
+                entityId,
+                entityWorkspaceAccessService.findAccessibleEntityForRequestWorkspace(entityId),
+                relationPreviewLimit);
+    }
+
     public EntityDto loadEntityDto(long entityId, String requestWorkspaceId) {
         return buildEntityDto(entityId, entityWorkspaceAccessService.findAccessibleEntityById(
                 entityId, requestWorkspaceId));
     }
 
+    public EntityDto loadEntityDto(long entityId, String requestWorkspaceId, int relationPreviewLimit) {
+        return buildEntityDto(
+                entityId,
+                entityWorkspaceAccessService.findAccessibleEntityById(entityId, requestWorkspaceId),
+                relationPreviewLimit);
+    }
+
+    public long countEntityRelations(long entityId) {
+        return entityRelationService.countEntityRelations(entityId);
+    }
+
     private EntityDto buildEntityDto(long entityId, Optional<ObserveEntity> optional) {
+        return buildEntityDto(entityId, optional, null);
+    }
+
+    private EntityDto buildEntityDto(long entityId, Optional<ObserveEntity> optional, Integer relationPreviewLimit) {
         if (optional.isEmpty()) {
             return null;
         }
@@ -61,7 +83,9 @@ public class EntityDetailReadModelService {
         dto.setEntity(entity);
         dto.setIdentities(entityIdentityReadModelService.findIdentities(entityId));
         dto.setMonitorBinds(entityMonitorBindService.findMonitorBinds(entityId));
-        dto.setRelations(entityRelationService.findEntityRelations(entityId));
+        dto.setRelations(relationPreviewLimit == null
+                ? entityRelationService.findEntityRelations(entityId)
+                : entityRelationService.findEntityRelations(entityId, relationPreviewLimit));
         return dto;
     }
 }
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
index ef5cc05750..195dc52cbb 100644
--- 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
@@ -18,10 +18,13 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
 import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
 
 /**
  * Query boundary for raw persisted entity identity rows, counts, and match lookups.
@@ -50,4 +53,16 @@ public class EntityIdentityQueryService {
     public long countIdentities(long entityId) {
         return entityIdentityDao.countByEntityId(entityId);
     }
+
+    public Map<Long, Long> countIdentitiesByEntityIds(List<Long> entityIds) {
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Map.of();
+        }
+        return entityIdentityDao.countByEntityIdInGroupByEntityId(entityIds)
+                .stream()
+                .collect(Collectors.toMap(
+                        row -> (Long) row[0],
+                        row -> ((Number) row[1]).longValue()
+                ));
+    }
 }
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
index 93f9b6dd21..29c93831b6 100644
--- 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
@@ -18,6 +18,7 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.springframework.stereotype.Service;
@@ -45,4 +46,8 @@ public class EntityIdentityReadModelService {
     public long countIdentities(long entityId) {
         return entityIdentityQueryService.countIdentities(entityId);
     }
+
+    public Map<Long, Long> countIdentitiesByEntityIds(List<Long> entityIds) {
+        return entityIdentityQueryService.countIdentitiesByEntityIds(entityIds);
+    }
 }
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
index 93182b7d75..aa2266b890 100644
--- 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
@@ -41,11 +41,14 @@ public class EntityIdentityWriteModelService {
 
     private final EntityIdentityDao entityIdentityDao;
     private final EntityIdentityResolutionService entityIdentityResolutionService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
 
     public EntityIdentityWriteModelService(EntityIdentityDao entityIdentityDao,
-                                           EntityIdentityResolutionService entityIdentityResolutionService) {
+                                           EntityIdentityResolutionService entityIdentityResolutionService,
+                                           EntityWorkspaceAccessService entityWorkspaceAccessService) {
         this.entityIdentityDao = entityIdentityDao;
         this.entityIdentityResolutionService = entityIdentityResolutionService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
     }
 
     public void replaceIdentities(ObserveEntity entity, List<EntityIdentity> identities) {
@@ -86,6 +89,7 @@ public class EntityIdentityWriteModelService {
         if (rows.isEmpty()) {
             rows.addAll(buildDefaultIdentities(entity));
         }
+        rejectPrimaryIdentityCollisions(entity, rows);
         entityIdentityDao.saveAll(rows);
     }
 
@@ -173,6 +177,56 @@ public class EntityIdentityWriteModelService {
         };
     }
 
+    private void rejectPrimaryIdentityCollisions(ObserveEntity entity, List<EntityIdentity> rows) {
+        List<EntityIdentity> primaryRows = rows.stream()
+                .filter(EntityIdentity::isPrimaryIdentity)
+                .filter(identity -> StringUtils.hasText(identity.getIdentityKey())
+                        && StringUtils.hasText(identity.getNormalizedValue()))
+                .toList();
+        if (primaryRows.isEmpty()) {
+            return;
+        }
+        Set<String> identityKeys = primaryRows.stream()
+                .map(EntityIdentity::getIdentityKey)
+                .collect(java.util.stream.Collectors.toSet());
+        Set<String> normalizedValues = primaryRows.stream()
+                .map(EntityIdentity::getNormalizedValue)
+                .collect(java.util.stream.Collectors.toSet());
+        List<EntityIdentity> matchingIdentities =
+                entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(identityKeys, normalizedValues);
+        if (CollectionUtils.isEmpty(matchingIdentities)) {
+            return;
+        }
+        Set<Long> matchingEntityIds = matchingIdentities.stream()
+                .map(EntityIdentity::getEntityId)
+                .filter(id -> id != null && !id.equals(entity.getId()))
+                .collect(java.util.stream.Collectors.toSet());
+        if (matchingEntityIds.isEmpty()) {
+            return;
+        }
+        Set<Long> accessibleEntityIds = entityWorkspaceAccessService
+                .findAccessibleEntitiesByIdsForRequestWorkspace(matchingEntityIds)
+                .stream()
+                .map(ObserveEntity::getId)
+                .collect(java.util.stream.Collectors.toSet());
+        if (accessibleEntityIds.isEmpty()) {
+            return;
+        }
+        for (EntityIdentity primaryRow : primaryRows) {
+            for (EntityIdentity matchingIdentity : matchingIdentities) {
+                if (!accessibleEntityIds.contains(matchingIdentity.getEntityId())) {
+                    continue;
+                }
+                if (primaryRow.getIdentityKey().equals(matchingIdentity.getIdentityKey())
+                        && primaryRow.getNormalizedValue().equals(matchingIdentity.getNormalizedValue())
+                        && matchingIdentity.isPrimaryIdentity()) {
+                    throw new IllegalArgumentException("Entity primary identity already exists: "
+                            + primaryRow.getIdentityKey() + "=" + primaryRow.getIdentityValue() + ".");
+                }
+            }
+        }
+    }
+
     private String defaultText(String... values) {
         for (String value : values) {
             if (StringUtils.hasText(value)) {
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
index 56ca09f10b..52d62b3a8d 100644
--- 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
@@ -18,10 +18,18 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 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.common.entity.manager.EntityMonitorBind;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.apache.hertzbeat.manager.pojo.dto.EntityMonitorBindingCandidate;
 import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
 
 /**
  * Builds integration hints that connect traditional monitors to entity catalog candidates.
@@ -29,13 +37,21 @@ import org.springframework.stereotype.Service;
 @Service
 public class EntityIntegrationHintService {
 
+    private static final String RECOMMEND_ALREADY_BOUND = "already_bound";
+
     private final EntityMonitorQueryService entityMonitorQueryService;
     private final EntityIdentityResolutionService entityIdentityResolutionService;
+    private final EntityMonitorBindService entityMonitorBindService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
 
     public EntityIntegrationHintService(EntityMonitorQueryService entityMonitorQueryService,
-                                        EntityIdentityResolutionService entityIdentityResolutionService) {
+                                        EntityIdentityResolutionService entityIdentityResolutionService,
+                                        EntityMonitorBindService entityMonitorBindService,
+                                        EntityWorkspaceAccessService entityWorkspaceAccessService) {
         this.entityMonitorQueryService = entityMonitorQueryService;
         this.entityIdentityResolutionService = entityIdentityResolutionService;
+        this.entityMonitorBindService = entityMonitorBindService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
     }
 
     public List<EntityMonitorBindingCandidate> getMonitorBindingCandidates(long monitorId) {
@@ -43,6 +59,57 @@ public class EntityIntegrationHintService {
         if (monitor == null) {
             return Collections.emptyList();
         }
-        return entityIdentityResolutionService.resolveMonitorBindingCandidates(monitor);
+        return includeExistingBoundEntities(monitor.getId(),
+                entityIdentityResolutionService.resolveMonitorBindingCandidates(monitor));
+    }
+
+    public Map<Long, List<EntityMonitorBindingCandidate>> getMonitorBindingCandidates(List<Long> monitorIds) {
+        if (CollectionUtils.isEmpty(monitorIds)) {
+            return Collections.emptyMap();
+        }
+        Map<Long, List<EntityMonitorBindingCandidate>> result = new LinkedHashMap<>();
+        monitorIds.stream()
+                .filter(java.util.Objects::nonNull)
+                .filter(monitorId -> monitorId > 0)
+                .distinct()
+                .limit(100)
+                .forEach(monitorId -> result.put(monitorId, getMonitorBindingCandidates(monitorId)));
+        return result;
+    }
+
+    private List<EntityMonitorBindingCandidate> includeExistingBoundEntities(
+            long monitorId, List<EntityMonitorBindingCandidate> identityCandidates) {
+        List<EntityMonitorBindingCandidate> candidates = new java.util.ArrayList<>(
+                identityCandidates == null ? Collections.emptyList() : identityCandidates);
+        Set<Long> candidateEntityIds = candidates.stream()
+                .map(EntityMonitorBindingCandidate::getEntityId)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        Set<Long> boundEntityIds = entityMonitorBindService.findMonitorBindsByMonitorId(monitorId).stream()
+                .map(EntityMonitorBind::getEntityId)
+                .filter(java.util.Objects::nonNull)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        boundEntityIds.removeAll(candidateEntityIds);
+        if (CollectionUtils.isEmpty(boundEntityIds)) {
+            return candidates;
+        }
+        Map<Long, ObserveEntity> entities = entityWorkspaceAccessService
+                .findAccessibleEntitiesByIdsForRequestWorkspace(boundEntityIds)
+                .stream()
+                .collect(Collectors.toMap(ObserveEntity::getId, item -> item, (left, right) -> left));
+        for (Long entityId : boundEntityIds) {
+            ObserveEntity entity = entities.get(entityId);
+            if (entity == null) {
+                continue;
+            }
+            candidates.add(new EntityMonitorBindingCandidate(
+                    entity.getId(),
+                    entity.getDisplayName() == null ? entity.getName() : entity.getDisplayName(),
+                    entity.getType(),
+                    0,
+                    RECOMMEND_ALREADY_BOUND,
+                    true,
+                    Collections.emptyMap()));
+        }
+        return candidates;
     }
 }
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
index ef6d77623c..8948c10fa4 100644
--- 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
@@ -34,6 +34,9 @@ import org.springframework.stereotype.Service;
 @Service
 public class EntityListReadModelService {
 
+    static final int DEFAULT_ENTITY_LIST_PAGE_SIZE = 8;
+    static final int MAX_ENTITY_LIST_PAGE_SIZE = 50;
+
     private final EntityCatalogQueryService entityCatalogQueryService;
     private final EntityStatusRefreshService entityStatusRefreshService;
     private final EntitySummaryReadModelService entitySummaryReadModelService;
@@ -50,9 +53,11 @@ public class EntityListReadModelService {
                                                String owner, String source, String environment, String lifecycle,
                                                String tier, String system, String sort, String order,
                                                int pageIndex, int pageSize) {
+        int safePageIndex = normalizePageIndex(pageIndex);
+        int safePageSize = normalizePageSize(pageSize);
         Page<ObserveEntity> entityPage = entityCatalogQueryService.findEntityPage(
                 entityIds, type, status, search, owner, source, environment, lifecycle, tier, system,
-                sort, order, pageIndex, pageSize);
+                sort, order, safePageIndex, safePageSize);
         return buildEntitySummaryPage(entityPage, null, false);
     }
 
@@ -60,42 +65,51 @@ public class EntityListReadModelService {
                                                String owner, String source, String environment, String lifecycle,
                                                String tier, String system, String sort, String order,
                                                int pageIndex, int pageSize, String requestWorkspaceId) {
+        int safePageIndex = normalizePageIndex(pageIndex);
+        int safePageSize = normalizePageSize(pageSize);
         Page<ObserveEntity> entityPage = entityCatalogQueryService.findEntityPage(
                 entityIds, type, status, search, owner, source, environment, lifecycle, tier, system,
-                sort, order, pageIndex, pageSize, requestWorkspaceId);
+                sort, order, safePageIndex, safePageSize, requestWorkspaceId);
         return buildEntitySummaryPage(entityPage, requestWorkspaceId, true);
     }
 
+    private int normalizePageIndex(int pageIndex) {
+        return Math.max(0, pageIndex);
+    }
+
+    private int normalizePageSize(int pageSize) {
+        if (pageSize <= 0) {
+            return DEFAULT_ENTITY_LIST_PAGE_SIZE;
+        }
+        return Math.min(pageSize, MAX_ENTITY_LIST_PAGE_SIZE);
+    }
+
     private Page<EntitySummaryInfo> buildEntitySummaryPage(Page<ObserveEntity> entityPage,
                                                            String requestWorkspaceId,
                                                            boolean explicitWorkspace) {
         List<ObserveEntity> scopedEntities = entityPage.getContent();
         Map<Long, EntityDefinitionActivity> latestActivityMap =
                 entitySummaryReadModelService.loadLatestDefinitionActivities(scopedEntities);
+        Map<Long, EntitySummaryReadModelService.EntitySummaryCounts> summaryCountsMap =
+                entitySummaryReadModelService.loadSummaryCounts(scopedEntities);
+        Map<Long, EntityStatusRefreshService.EntityRuntimeStatusEvidence> runtimeEvidenceMap =
+                explicitWorkspace
+                        ? entityStatusRefreshService.refreshEntityStatusesWithEvidence(scopedEntities, requestWorkspaceId)
+                        : entityStatusRefreshService.refreshEntityStatusesWithEvidence(scopedEntities);
         List<EntitySummaryInfo> summaries = scopedEntities.stream()
-                .map(entity -> explicitWorkspace
-                        ? buildSummary(entity, latestActivityMap.get(entity.getId()), requestWorkspaceId)
-                        : buildSummary(entity, latestActivityMap.get(entity.getId())))
+                .map(entity -> buildSummary(entity, latestActivityMap.get(entity.getId()),
+                        summaryCountsMap.get(entity.getId()), runtimeEvidenceMap.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);
+                                           EntitySummaryReadModelService.EntitySummaryCounts summaryCounts,
+                                           EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence) {
         List<Monitor> monitors = runtimeEvidence.monitors();
         EntityStatusInfo statusInfo = runtimeEvidence.statusInfo();
-        return entitySummaryReadModelService.buildEntitySummary(entity, latestActivity, statusInfo, monitors);
+        return entitySummaryReadModelService.buildEntitySummary(entity, latestActivity, statusInfo, monitors,
+                summaryCounts);
     }
 }
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
index 824e467683..b1ebd016c0 100644
--- 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
@@ -18,10 +18,15 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
 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;
 
 /**
  * Owns persisted monitor-bind evidence lookup for entity read-model boundaries.
@@ -39,6 +44,29 @@ public class EntityMonitorBindQueryService {
         return entityMonitorBindDao.findAllByEntityIdOrderByIdAsc(entityId);
     }
 
+    public Map<Long, List<EntityMonitorBind>> findMonitorBindsByEntityIds(List<Long> entityIds) {
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Map.of();
+        }
+        List<Long> acceptedEntityIds = entityIds.stream()
+                .filter(Objects::nonNull)
+                .distinct()
+                .toList();
+        if (CollectionUtils.isEmpty(acceptedEntityIds)) {
+            return Map.of();
+        }
+        Map<Long, List<EntityMonitorBind>> bindsByEntityId = new LinkedHashMap<>();
+        entityMonitorBindDao.findAllByEntityIdInOrderByEntityIdAscIdAsc(acceptedEntityIds)
+                .forEach(bind -> {
+                    if (bind == null || bind.getEntityId() == null) {
+                        return;
+                    }
+                    bindsByEntityId.computeIfAbsent(bind.getEntityId(), ignored -> new java.util.ArrayList<>())
+                            .add(bind);
+                });
+        return bindsByEntityId;
+    }
+
     public List<EntityMonitorBind> findMonitorBindsByMonitorId(Long monitorId) {
         if (monitorId == null) {
             return Collections.emptyList();
@@ -49,4 +77,16 @@ public class EntityMonitorBindQueryService {
     public long countMonitorBinds(Long entityId) {
         return entityMonitorBindDao.countByEntityId(entityId);
     }
+
+    public Map<Long, Long> countMonitorBindsByEntityIds(List<Long> entityIds) {
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Map.of();
+        }
+        return entityMonitorBindDao.countByEntityIdInGroupByEntityId(entityIds)
+                .stream()
+                .collect(Collectors.toMap(
+                        row -> (Long) row[0],
+                        row -> ((Number) row[1]).longValue()
+                ));
+    }
 }
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
index 6358af3429..51843e5532 100644
--- 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
@@ -75,13 +75,22 @@ public class EntityMonitorBindService {
         return entityMonitorBindQueryService.countMonitorBinds(entityId);
     }
 
+    public Map<Long, Long> countMonitorBindsByEntityIds(List<Long> entityIds) {
+        return entityMonitorBindQueryService.countMonitorBindsByEntityIds(entityIds);
+    }
+
     public void replaceMonitorBinds(Long entityId, List<EntityMonitorBind> monitorBinds) {
         List<EntityMonitorBind> rows = new ArrayList<>();
+        Set<Long> acceptedMonitorIds = new LinkedHashSet<>();
         if (!CollectionUtils.isEmpty(monitorBinds)) {
             for (EntityMonitorBind bind : monitorBinds) {
                 if (bind == null || !entityMonitorQueryService.monitorExists(bind.getMonitorId())) {
                     continue;
                 }
+                if (!acceptedMonitorIds.add(bind.getMonitorId())) {
+                    continue;
+                }
+                requireMonitorAvailableForEntity(entityId, bind.getMonitorId());
                 rows.add(EntityMonitorBind.builder()
                         .entityId(entityId)
                         .monitorId(bind.getMonitorId())
@@ -96,6 +105,16 @@ public class EntityMonitorBindService {
         entityMonitorBindWriteModelService.replaceMonitorBinds(entityId, rows);
     }
 
+    private void requireMonitorAvailableForEntity(Long entityId, Long monitorId) {
+        List<EntityMonitorBind> existingBinds = entityMonitorBindQueryService.findMonitorBindsByMonitorId(monitorId);
+        for (EntityMonitorBind existingBind : existingBinds) {
+            if (existingBind == null || Objects.equals(entityId, existingBind.getEntityId())) {
+                continue;
+            }
+            throw new IllegalArgumentException("Monitor already bound to another entity: " + monitorId + ".");
+        }
+    }
+
     public void replaceAutoMonitorBinds(Long monitorId, List<EntityMonitorBindingCandidate> candidates) {
         if (monitorId == null) {
             return;
@@ -129,6 +148,47 @@ public class EntityMonitorBindService {
 
     public List<Monitor> findEntityMonitors(Long entityId) {
         List<EntityMonitorBind> binds = entityMonitorBindQueryService.findMonitorBinds(entityId);
+        return resolveMonitorsInBindOrder(binds);
+    }
+
+    public Map<Long, List<Monitor>> findEntityMonitorsByEntityIds(List<Long> entityIds) {
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Collections.emptyMap();
+        }
+        Map<Long, List<EntityMonitorBind>> bindsByEntityId =
+                entityMonitorBindQueryService.findMonitorBindsByEntityIds(entityIds);
+        Set<Long> monitorIds = bindsByEntityId.values()
+                .stream()
+                .flatMap(List::stream)
+                .map(EntityMonitorBind::getMonitorId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        if (monitorIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<Long, Monitor> monitorMap = entityMonitorQueryService.findMonitorsByIds(monitorIds).stream()
+                .collect(Collectors.toMap(Monitor::getId, item -> item, (left, right) -> left));
+        Map<Long, List<Monitor>> monitorsByEntityId = new java.util.LinkedHashMap<>();
+        for (Long entityId : entityIds) {
+            List<EntityMonitorBind> binds = bindsByEntityId.get(entityId);
+            if (CollectionUtils.isEmpty(binds)) {
+                continue;
+            }
+            List<Monitor> monitors = new ArrayList<>();
+            for (EntityMonitorBind bind : binds) {
+                Monitor monitor = monitorMap.get(bind.getMonitorId());
+                if (monitor != null) {
+                    monitors.add(monitor);
+                }
+            }
+            if (!monitors.isEmpty()) {
+                monitorsByEntityId.put(entityId, monitors);
+            }
+        }
+        return monitorsByEntityId;
+    }
+
+    private List<Monitor> resolveMonitorsInBindOrder(List<EntityMonitorBind> binds) {
         if (CollectionUtils.isEmpty(binds)) {
             return Collections.emptyList();
         }
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
index c0015577f4..5c1e25aa79 100644
--- 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
@@ -18,12 +18,23 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 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.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
 
 /**
  * Coordinates entity write workflows while dedicated components own each write stage.
@@ -35,6 +46,8 @@ public class EntityMutationWorkflowService {
     private static final String ACTIVITY_TYPE_DEFINITION_IMPORT = "definition_import";
     private static final String ACTIVITY_TYPE_DEFINITION_UPDATE = "definition_update";
 
+    private final ConcurrentMap<String, ReentrantLock> mutationReferenceLocks = new ConcurrentHashMap<>();
+
     private final EntityCoreWriteModelService entityCoreWriteModelService;
     private final EntityIdentityWriteModelService entityIdentityWriteModelService;
     private final EntityMonitorBindService entityMonitorBindService;
@@ -66,7 +79,10 @@ public class EntityMutationWorkflowService {
     }
 
     public long addEntity(EntityDto entityDto) {
-        return addEntity(entityDto, true);
+        return withMutationReferenceLocks(List.of(entityDto), () -> {
+            requireNewEntityReferences(List.of(entityDto));
+            return addEntity(entityDto, true);
+        });
     }
 
     public void modifyEntity(EntityDto entityDto) {
@@ -76,13 +92,16 @@ public class EntityMutationWorkflowService {
     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;
+        return withMutationReferenceLocks(List.of(entityDto), () -> {
+            requireNewEntityReferences(List.of(entityDto));
+            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) {
@@ -93,31 +112,158 @@ public class EntityMutationWorkflowService {
         if (entityDtos.size() == 1) {
             EntityDto entityDto = entityDtos.getFirst();
             entityValidationService.validate(entityDto, false);
-            return List.of(addEntity(entityDto));
+            return withMutationReferenceLocks(List.of(entityDto), () -> {
+                requireNewEntityReferences(List.of(entityDto));
+                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());
+        return withMutationReferenceLocks(entityDtos, () -> {
+            requireNewEntityReferences(entityDtos);
+            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();
+        });
+    }
+
+    private <T> T withMutationReferenceLocks(List<EntityDto> entityDtos, Supplier<T> supplier) {
+        List<String> lockKeys = mutationLockKeys(entityDtos);
+        if (lockKeys.isEmpty()) {
+            return supplier.get();
+        }
+        List<ReentrantLock> locks = lockKeys.stream()
+                .map(key -> mutationReferenceLocks.computeIfAbsent(key, ignored -> new ReentrantLock()))
+                .toList();
+        locks.forEach(ReentrantLock::lock);
+        boolean releaseAfterTransaction = TransactionSynchronizationManager.isSynchronizationActive();
+        if (releaseAfterTransaction) {
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCompletion(int status) {
+                    releaseMutationLocks(lockKeys, locks);
+                }
+            });
         }
-        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);
+        try {
+            return supplier.get();
+        } finally {
+            if (!releaseAfterTransaction) {
+                releaseMutationLocks(lockKeys, locks);
+            }
+        }
+    }
+
+    private void releaseMutationLocks(List<String> lockKeys, List<ReentrantLock> locks) {
+        for (int index = locks.size() - 1; index >= 0; index--) {
+            ReentrantLock lock = locks.get(index);
+            String lockKey = lockKeys.get(index);
+            try {
+                lock.unlock();
+            } finally {
+                if (!lock.isLocked() && !lock.hasQueuedThreads()) {
+                    mutationReferenceLocks.remove(lockKey, lock);
+                }
+            }
+        }
+    }
+
+    private List<String> mutationLockKeys(List<EntityDto> entityDtos) {
+        Set<String> lockKeys = new HashSet<>();
+        for (EntityDto entityDto : entityDtos) {
+            if (entityDto == null) {
+                continue;
+            }
+            ObserveEntity entity = entityDto.getEntity();
+            if (entity != null && StringUtils.hasText(entity.getType()) && StringUtils.hasText(entity.getName())) {
+                lockKeys.add("entity:" + referenceKey(entity));
+            }
+            if (entity != null && StringUtils.hasText(entity.getName())) {
+                lockKeys.add("entity-name:" + normalizeReferencePart(entity.getName()));
+            }
+            if (!CollectionUtils.isEmpty(entityDto.getIdentities())) {
+                for (EntityIdentity identity : entityDto.getIdentities()) {
+                    if (identity == null
+                            || !identity.isPrimaryIdentity()
+                            || !StringUtils.hasText(identity.getIdentityKey())) {
+                        continue;
+                    }
+                    String identityValue = StringUtils.hasText(identity.getNormalizedValue())
+                            ? identity.getNormalizedValue()
+                            : identity.getIdentityValue();
+                    if (StringUtils.hasText(identityValue)) {
+                        lockKeys.add("primary-identity:"
+                                + normalizeReferencePart(identity.getIdentityKey())
+                                + '\n' + normalizeReferencePart(identityValue));
+                    }
+                }
+            }
+        }
+        return lockKeys.stream().sorted().toList();
+    }
+
+    private void requireNewEntityReferences(List<EntityDto> entityDtos) {
+        Set<String> submittedReferences = new HashSet<>();
+        Set<String> submittedNames = new HashSet<>();
+        for (EntityDto entityDto : entityDtos) {
+            ObserveEntity entity = entityDto.getEntity();
+            if (entity == null || !StringUtils.hasText(entity.getType()) || !StringUtils.hasText(entity.getName())) {
+                continue;
+            }
+            String entityName = entity.getName().trim();
+            String nameKey = normalizeReferencePart(entityName);
+            if (!submittedNames.add(nameKey)) {
+                throw new IllegalArgumentException("Entity already exists in definition bundle: " + entityName + ".");
+            }
+            if (entityWorkspaceAccessService.findAccessibleEntityByNameForRequestWorkspace(entityName).isPresent()) {
+                throw new IllegalArgumentException("Entity already exists: " + entityName + ".");
+            }
+            String referenceKey = referenceKey(entity);
+            if (!submittedReferences.add(referenceKey)) {
+                throw new IllegalArgumentException("Entity already exists in definition bundle: " + displayReference(entity) + ".");
+            }
+            boolean exists = StringUtils.hasText(entity.getNamespace())
+                    ? entityWorkspaceAccessService.findAccessibleEntityByReferenceForRequestWorkspace(
+                            entity.getType(), entity.getNamespace(), entity.getName()).isPresent()
+                    : entityWorkspaceAccessService.findAccessibleEntityByReferenceForRequestWorkspace(
+                            entity.getType(), entity.getName()).isPresent();
+            if (exists) {
+                throw new IllegalArgumentException("Entity already exists: " + displayReference(entity) + ".");
+            }
         }
-        return entities.stream().map(ObserveEntity::getId).toList();
+    }
+
+    private String referenceKey(ObserveEntity entity) {
+        return normalizeReferencePart(entity.getType())
+                + '\n' + normalizeReferencePart(entity.getNamespace())
+                + '\n' + normalizeReferencePart(entity.getName());
+    }
+
+    private String displayReference(ObserveEntity entity) {
+        String namespacePrefix = StringUtils.hasText(entity.getNamespace()) ? entity.getNamespace().trim() + "/" : "";
+        return entity.getType().trim() + " " + namespacePrefix + entity.getName().trim();
+    }
+
+    private String normalizeReferencePart(String value) {
+        return StringUtils.hasText(value) ? value.trim().toLowerCase(Locale.ROOT) : "";
     }
 
     public void modifyEntityByDefinition(long entityId, EntityDefinitionRequest definitionRequest) {
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
index eb1059d710..9d2046c63b 100644
--- 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
@@ -51,15 +51,13 @@ public class EntityNoiseControlRuleQueryService {
     }
 
     public List<AlertSilence> findEnabledSilences(String requestWorkspaceId) {
-        return alertSilenceDao.findAll().stream()
-                .filter(rule -> Boolean.TRUE.equals(rule.isEnable()))
+        return alertSilenceDao.findAlertSilencesByEnableTrue().stream()
                 .filter(rule -> matchesSilenceRuleRequestWorkspace(rule, requestWorkspaceId))
                 .toList();
     }
 
     public List<AlertInhibit> findEnabledInhibits(String requestWorkspaceId) {
-        return alertInhibitDao.findAll().stream()
-                .filter(rule -> Boolean.TRUE.equals(rule.getEnable()))
+        return alertInhibitDao.findAlertInhibitsByEnableIsTrue().stream()
                 .filter(rule -> matchesInhibitRuleRequestWorkspace(rule, requestWorkspaceId))
                 .toList();
     }
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
index 1ff5f42eac..e03227591a 100644
--- 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
@@ -18,9 +18,14 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import org.apache.hertzbeat.common.entity.manager.EntityRelation;
 import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
 
 /**
  * Owns persisted relation evidence lookup for entity read-model boundaries.
@@ -38,7 +43,36 @@ public class EntityRelationQueryService {
         return entityRelationDao.findBySourceEntityIdOrTargetEntityId(entityId, entityId);
     }
 
+    public List<EntityRelation> findEntityRelations(Long entityId, int limit) {
+        if (limit <= 0) {
+            return List.of();
+        }
+        return entityRelationDao.findBySourceEntityIdOrTargetEntityId(
+                entityId, entityId, PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "id")));
+    }
+
     public long countEntityRelations(Long entityId) {
         return entityRelationDao.countBySourceEntityIdOrTargetEntityId(entityId, entityId);
     }
+
+    public Map<Long, Long> countEntityRelationsByEntityIds(List<Long> entityIds) {
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Map.of();
+        }
+        Map<Long, Long> counts = new LinkedHashMap<>();
+        mergeCounts(counts, entityRelationDao.countBySourceEntityIdInGroupBySourceEntityId(entityIds));
+        mergeCounts(counts, entityRelationDao.countByTargetEntityIdInGroupByTargetEntityId(entityIds));
+        return counts;
+    }
+
+    private void mergeCounts(Map<Long, Long> counts, List<Object[]> rows) {
+        if (CollectionUtils.isEmpty(rows)) {
+            return;
+        }
+        for (Object[] row : rows) {
+            Long entityId = (Long) row[0];
+            long count = ((Number) row[1]).longValue();
+            counts.merge(entityId, count, Long::sum);
+        }
+    }
 }
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
index 1df37194df..b970f2defa 100644
--- 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
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.manager.service.entity;
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.EntityRelation;
@@ -71,10 +72,18 @@ public class EntityRelationService {
         return entityRelationQueryService.findEntityRelations(entityId);
     }
 
+    public List<EntityRelation> findEntityRelations(Long entityId, int limit) {
+        return entityRelationQueryService.findEntityRelations(entityId, limit);
+    }
+
     public long countEntityRelations(Long entityId) {
         return entityRelationQueryService.countEntityRelations(entityId);
     }
 
+    public Map<Long, Long> countEntityRelationsByEntityIds(List<Long> entityIds) {
+        return entityRelationQueryService.countEntityRelationsByEntityIds(entityIds);
+    }
+
     public void replaceRelations(Long entityId, List<EntityRelation> relations, String requestWorkspaceId) {
         replaceRelationsInternal(entityId, relations, requestWorkspaceId, false);
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshService.java
index e48c19451a..ffd027a0bc 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshService.java
@@ -17,12 +17,17 @@
 
 package org.apache.hertzbeat.manager.service.entity;
 
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 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;
 
 /**
  * Refreshes entity runtime status from current monitor bindings and alert evidence.
@@ -54,20 +59,64 @@ public class EntityStatusRefreshService {
 
     public EntityRuntimeStatusEvidence refreshEntityStatusWithEvidence(ObserveEntity entity) {
         List<Monitor> monitors = entityMonitorBindService.findEntityMonitors(entity.getId());
-        List<SingleAlert> activeAlerts = entityAlertEvidenceReadModelService.queryActiveAlerts(
-                monitors, ACTIVE_ALERT_LIMIT);
-        EntityStatusInfo statusInfo = entityRuntimeHealthService.refreshEntityStatus(entity, monitors, activeAlerts);
-        return new EntityRuntimeStatusEvidence(monitors, activeAlerts, statusInfo);
+        return refreshEntityStatusWithEvidence(entity, monitors, null, false);
     }
 
     public EntityRuntimeStatusEvidence refreshEntityStatusWithEvidence(ObserveEntity entity, String requestWorkspaceId) {
         List<Monitor> monitors = entityMonitorBindService.findEntityMonitors(entity.getId());
-        List<SingleAlert> activeAlerts = entityAlertEvidenceReadModelService.queryActiveAlerts(
-                monitors, ACTIVE_ALERT_LIMIT, requestWorkspaceId);
+        return refreshEntityStatusWithEvidence(entity, monitors, requestWorkspaceId, true);
+    }
+
+    public Map<Long, EntityRuntimeStatusEvidence> refreshEntityStatusesWithEvidence(List<ObserveEntity> entities) {
+        return refreshEntityStatusesWithEvidence(entities, null, false);
+    }
+
+    public Map<Long, EntityRuntimeStatusEvidence> refreshEntityStatusesWithEvidence(List<ObserveEntity> entities,
+                                                                                    String requestWorkspaceId) {
+        return refreshEntityStatusesWithEvidence(entities, requestWorkspaceId, true);
+    }
+
+    private Map<Long, EntityRuntimeStatusEvidence> refreshEntityStatusesWithEvidence(List<ObserveEntity> entities,
+                                                                                    String requestWorkspaceId,
+                                                                                    boolean explicitWorkspace) {
+        if (CollectionUtils.isEmpty(entities)) {
+            return Collections.emptyMap();
+        }
+        List<Long> entityIds = entities.stream()
+                .map(ObserveEntity::getId)
+                .filter(Objects::nonNull)
+                .toList();
+        Map<Long, List<Monitor>> monitorsByEntityId = entityMonitorBindService.findEntityMonitorsByEntityIds(entityIds);
+        Map<Long, EntityRuntimeStatusEvidence> evidenceByEntityId = new LinkedHashMap<>();
+        for (ObserveEntity entity : entities) {
+            if (entity == null || entity.getId() == null) {
+                continue;
+            }
+            List<Monitor> monitors = monitorsByEntityId.getOrDefault(entity.getId(), Collections.emptyList());
+            evidenceByEntityId.put(entity.getId(),
+                    refreshEntityStatusWithEvidence(entity, monitors, requestWorkspaceId, explicitWorkspace));
+        }
+        return evidenceByEntityId;
+    }
+
+    private EntityRuntimeStatusEvidence refreshEntityStatusWithEvidence(ObserveEntity entity,
+                                                                        List<Monitor> monitors,
+                                                                        String requestWorkspaceId,
+                                                                        boolean explicitWorkspace) {
+        List<SingleAlert> activeAlerts = CollectionUtils.isEmpty(monitors)
+                ? Collections.emptyList()
+                : queryActiveAlerts(monitors, requestWorkspaceId, explicitWorkspace);
         EntityStatusInfo statusInfo = entityRuntimeHealthService.refreshEntityStatus(entity, monitors, activeAlerts);
         return new EntityRuntimeStatusEvidence(monitors, activeAlerts, statusInfo);
     }
 
+    private List<SingleAlert> queryActiveAlerts(List<Monitor> monitors, String requestWorkspaceId,
+                                                boolean explicitWorkspace) {
+        return explicitWorkspace
+                ? entityAlertEvidenceReadModelService.queryActiveAlerts(monitors, ACTIVE_ALERT_LIMIT, requestWorkspaceId)
+                : entityAlertEvidenceReadModelService.queryActiveAlerts(monitors, ACTIVE_ALERT_LIMIT);
+    }
+
     /**
      * Runtime status evidence collected during a refresh operation.
      */
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelService.java
index e4825a616d..71d5ff1ba8 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelService.java
@@ -62,29 +62,45 @@ public class EntitySummaryReadModelService {
                                                 EntityDefinitionActivity latestDefinitionActivity,
                                                 EntityStatusInfo statusInfo,
                                                 List<Monitor> monitors) {
+        return buildEntitySummary(
+                entity,
+                latestDefinitionActivity,
+                statusInfo,
+                monitors,
+                new EntitySummaryCounts(
+                        entityIdentityReadModelService.countIdentities(entity.getId()),
+                        entityMonitorBindService.countMonitorBinds(entity.getId()),
+                        entityRelationService.countEntityRelations(entity.getId())
+                )
+        );
+    }
+
+    public EntitySummaryInfo buildEntitySummary(ObserveEntity entity,
+                                                EntityDefinitionActivity latestDefinitionActivity,
+                                                EntityStatusInfo statusInfo,
+                                                List<Monitor> monitors,
+                                                EntitySummaryCounts counts) {
         List<Monitor> safeMonitors = CollectionUtils.isEmpty(monitors) ? Collections.emptyList() : monitors;
-        long monitorBindCount = entityMonitorBindService.countMonitorBinds(entity.getId());
-        long identityCount = entityIdentityReadModelService.countIdentities(entity.getId());
-        long relationCount = entityRelationService.countEntityRelations(entity.getId());
+        EntitySummaryCounts safeCounts = counts == null ? EntitySummaryCounts.empty() : counts;
         EntityEvidenceSummaryInfo evidenceSummary = entityObservabilityGateway.buildEntityEvidenceSummary(
                 entity,
                 statusInfo,
-                identityCount,
+                safeCounts.identityCount(),
                 0,
                 safeMonitors,
                 Collections.emptyList()
         );
         EntityOpsSummaryInfo opsSummary = entityObservabilityGateway.buildEntityOpsSummary(
-                entity, relationCount, evidenceSummary
+                entity, safeCounts.relationCount(), evidenceSummary
         );
         EntityNextActionInfo nextAction = entityObservabilityGateway.buildEntityNextActions(
                 entity, evidenceSummary, null, opsSummary
         ).stream().findFirst().orElse(null);
         return new EntitySummaryInfo(
                 EntityInfo.fromEntity(entity),
-                identityCount,
-                monitorBindCount,
-                relationCount,
+                safeCounts.identityCount(),
+                safeCounts.monitorBindCount(),
+                safeCounts.relationCount(),
                 statusInfo.getActiveAlertCount(),
                 statusInfo,
                 opsSummary,
@@ -98,6 +114,31 @@ public class EntitySummaryReadModelService {
         );
     }
 
+    public Map<Long, EntitySummaryCounts> loadSummaryCounts(List<ObserveEntity> entities) {
+        if (CollectionUtils.isEmpty(entities)) {
+            return Collections.emptyMap();
+        }
+        List<Long> entityIds = entities.stream()
+                .map(ObserveEntity::getId)
+                .filter(Objects::nonNull)
+                .toList();
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Collections.emptyMap();
+        }
+        Map<Long, Long> identityCounts = entityIdentityReadModelService.countIdentitiesByEntityIds(entityIds);
+        Map<Long, Long> monitorBindCounts = entityMonitorBindService.countMonitorBindsByEntityIds(entityIds);
+        Map<Long, Long> relationCounts = entityRelationService.countEntityRelationsByEntityIds(entityIds);
+        return entityIds.stream()
+                .collect(java.util.stream.Collectors.toMap(
+                        entityId -> entityId,
+                        entityId -> new EntitySummaryCounts(
+                                identityCounts.getOrDefault(entityId, 0L),
+                                monitorBindCounts.getOrDefault(entityId, 0L),
+                                relationCounts.getOrDefault(entityId, 0L)
+                        )
+                ));
+    }
+
     public Map<Long, EntityDefinitionActivity> loadLatestDefinitionActivities(List<ObserveEntity> entities) {
         if (CollectionUtils.isEmpty(entities)) {
             return Collections.emptyMap();
@@ -111,4 +152,14 @@ public class EntitySummaryReadModelService {
         }
         return entityActivityReadModelService.findLatestDefinitionActivities(entityIds);
     }
+
+    /**
+     * Current-page count snapshot used to avoid per-row summary count queries.
+     */
+    public record EntitySummaryCounts(long identityCount, long monitorBindCount, long relationCount) {
+
+        public static EntitySummaryCounts empty() {
+            return new EntitySummaryCounts(0, 0, 0);
+        }
+    }
 }
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 a71d0bf0cd..40ebb05d8b 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
@@ -130,6 +130,16 @@ public class EntityWorkspaceAccessService {
         return findAccessibleEntityByReference(currentRequestWorkspaceId(), type, name);
     }
 
+    public Optional<ObserveEntity> findAccessibleEntityByName(String requestWorkspaceId, String name) {
+        String normalizedWorkspaceId = normalizeWorkspaceId(requestWorkspaceId);
+        Optional<ObserveEntity> entity = entityWorkspaceQueryService.findEntityByName(normalizedWorkspaceId, name);
+        return entity.filter(candidate -> matchesRequestWorkspace(candidate, normalizedWorkspaceId));
+    }
+
+    public Optional<ObserveEntity> findAccessibleEntityByNameForRequestWorkspace(String name) {
+        return findAccessibleEntityByName(currentRequestWorkspaceId(), name);
+    }
+
     public boolean matchesRequestWorkspace(ObserveEntity entity, String requestWorkspaceId) {
         if (!StringUtils.hasText(requestWorkspaceId)) {
             return true;
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 25fa541198..e3d2a52ba5 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
@@ -84,4 +84,11 @@ public class EntityWorkspaceQueryService {
         }
         return observeEntityDao.findFirstByTypeAndName(type, name);
     }
+
+    public Optional<ObserveEntity> findEntityByName(String workspaceId, String name) {
+        if (StringUtils.hasText(workspaceId)) {
+            return observeEntityDao.findFirstByWorkspaceIdAndName(workspaceId, name);
+        }
+        return observeEntityDao.findFirstByName(name);
+    }
 }
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
index 1b2e965a4b..04fe9f0c9c 100644
--- 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
@@ -129,18 +129,20 @@ public class LocalTopologyDemoRelationSeeder implements ApplicationRunner {
         List<ObserveEntity> missingEntities = new ArrayList<>();
         List<EntityIdentity> missingIdentities = new ArrayList<>();
         List<String> serviceNames = mixedScaleProofServiceNames();
+        Map<String, ObserveEntity> entitiesByName = new java.util.LinkedHashMap<>();
 
         for (int index = 0; index < serviceNames.size(); index += 1) {
             String serviceName = serviceNames.get(index);
             Optional<ObserveEntity> existingEntity = findMixedScaleProofEntity(serviceName);
             if (existingEntity.isPresent()) {
+                entitiesByName.put(serviceName, existingEntity.get());
                 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()
+            ObserveEntity entity = ObserveEntity.builder()
                     .id(entityId)
                     .workspaceId(WORKSPACE_ID)
                     .type("service")
@@ -148,11 +150,14 @@ public class LocalTopologyDemoRelationSeeder implements ApplicationRunner {
                     .name(serviceName)
                     .displayName(displayName(serviceName))
                     .environment(ENVIRONMENT)
+                    .source("local-scale-proof")
                     .status("unknown")
                     .criticality("medium")
                     .owner("local-scale-proof")
                     .lifecycle("local")
-                    .build());
+                    .build();
+            missingEntities.add(entity);
+            entitiesByName.put(serviceName, entity);
             missingIdentities.add(serviceNameIdentity(entityId, serviceName));
         }
 
@@ -165,6 +170,69 @@ public class LocalTopologyDemoRelationSeeder implements ApplicationRunner {
             log.info("Seeded {} local mixed topology scale proof service.name identity row(s).",
                     missingIdentities.size());
         }
+        seedMixedScaleProofRelations(entitiesByName);
+    }
+
+    void seedMixedScaleProofRelations(Map<String, ObserveEntity> entitiesByName) {
+        if (entitiesByName == null || entitiesByName.isEmpty()) {
+            return;
+        }
+        List<EntityRelation> missingRelations = new ArrayList<>();
+        String gatewayName = MIXED_PROOF_BATCH + "-edge-gateway";
+        ObserveEntity gateway = entitiesByName.get(gatewayName);
+        if (gateway == null || gateway.getId() == null) {
+            return;
+        }
+        Map<Long, List<EntityRelation>> relationCacheBySource = new java.util.LinkedHashMap<>();
+
+        for (int domainIndex = 0; domainIndex < 12; domainIndex += 1) {
+            String domainName = mixedScaleProofDomainName(domainIndex);
+            ObserveEntity domain = entitiesByName.get(domainName);
+            if (domain == null || domain.getId() == null) {
+                continue;
+            }
+            addMissingMixedScaleProofRelation(
+                    relationCacheBySource,
+                    missingRelations, gateway, domain, "trace-call", "otlp-trace-call",
+                    "local mixed topology scale proof gateway to domain call");
+            for (int serviceIndex = 0; serviceIndex < 165; serviceIndex += 1) {
+                ObserveEntity service = entitiesByName.get(mixedScaleProofServiceName(domainIndex, serviceIndex));
+                if (service == null || service.getId() == null) {
+                    continue;
+                }
+                addMissingMixedScaleProofRelation(
+                        relationCacheBySource,
+                        missingRelations, domain, service, "trace-call", "otlp-trace-call",
+                        "local mixed topology scale proof domain to service call");
+            }
+        }
+
+        if (!missingRelations.isEmpty()) {
+            entityRelationDao.saveAll(missingRelations);
+            log.info("Seeded {} local mixed topology scale proof relation row(s).", missingRelations.size());
+        }
+    }
+
+    private void addMissingMixedScaleProofRelation(Map<Long, List<EntityRelation>> relationCacheBySource,
+                                                   List<EntityRelation> missingRelations,
+                                                   ObserveEntity source,
+                                                   ObserveEntity target,
+                                                   String relationType,
+                                                   String relationSource,
+                                                   String description) {
+        if (!relationExists(relationCacheBySource, source.getId(), target.getId(), relationType)) {
+            missingRelations.add(EntityRelation.builder()
+                    .sourceEntityId(source.getId())
+                    .targetEntityId(target.getId())
+                    .targetRef("%s:%s/%s".formatted(target.getType(), target.getNamespace(), target.getName()))
+                    .relationType(relationType)
+                    .relationSource(relationSource)
+                    .status("confirmed")
+                    .score(92)
+                    .description(description)
+                    .attributes(seedAttributes("outbound"))
+                    .build());
+        }
     }
 
     private boolean hasServiceNameIdentity(Long entityId, String serviceName) {
@@ -201,16 +269,24 @@ public class LocalTopologyDemoRelationSeeder implements ApplicationRunner {
         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));
+            serviceNames.add(mixedScaleProofDomainName(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));
+                serviceNames.add(mixedScaleProofServiceName(domainIndex, serviceIndex));
             }
         }
         return serviceNames;
     }
 
+    private String mixedScaleProofDomainName(int domainIndex) {
+        return "%s-domain-%02d".formatted(MIXED_PROOF_BATCH, domainIndex);
+    }
+
+    private String mixedScaleProofServiceName(int domainIndex, int serviceIndex) {
+        return "%s-svc-%02d-%03d".formatted(MIXED_PROOF_BATCH, domainIndex, serviceIndex);
+    }
+
     private String displayName(String serviceName) {
         if (serviceName.equals(MIXED_PROOF_BATCH + "-edge-gateway")) {
             return "Mixed Edge Gateway";
@@ -229,6 +305,18 @@ public class LocalTopologyDemoRelationSeeder implements ApplicationRunner {
                         && relationType.equals(relation.getRelationType()));
     }
 
+    private boolean relationExists(Map<Long, List<EntityRelation>> relationCacheBySource,
+                                   Long sourceEntityId,
+                                   Long targetEntityId,
+                                   String relationType) {
+        List<EntityRelation> sourceRelations = relationCacheBySource.computeIfAbsent(sourceEntityId,
+                source -> entityRelationDao.findBySourceEntityIdOrTargetEntityId(source, source));
+        return sourceRelations.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,
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
index 7bf85944de..b058954b50 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
@@ -35,6 +35,7 @@ import org.apache.hertzbeat.base.service.LabelService;
 import org.springframework.beans.BeanUtils;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
 
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -104,6 +105,7 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
         var exportMonitor = new ExportMonitorDTO();
         var monitor = new MonitorDTO();
         BeanUtils.copyProperties(dto.getMonitor(), monitor);
+        monitor.setHost(dto.getMonitor().getInstance());
         exportMonitor.setMonitor(monitor);
         exportMonitor.setParams(dto.getParams().stream().map(it -> {
             var param = new ParamDTO();
@@ -128,6 +130,9 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
             // Add one more null check
             BeanUtils.copyProperties(exportMonitor.monitor, monitor);
         }
+        if (!StringUtils.hasText(monitor.getInstance())) {
+            monitor.setInstance(resolveImportedInstance(exportMonitor));
+        }
         monitorDto.setMonitor(monitor);
         if (exportMonitor.getMonitor() != null) {
             monitorDto.setCollector(exportMonitor.getMonitor().getCollector());
@@ -146,6 +151,22 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
         return monitorDto;
     }
 
+    private static String resolveImportedInstance(ExportMonitorDTO exportMonitor) {
+        if (exportMonitor.getMonitor() != null && StringUtils.hasText(exportMonitor.getMonitor().getHost())) {
+            return exportMonitor.getMonitor().getHost().trim();
+        }
+        if (exportMonitor.getParams() == null) {
+            return null;
+        }
+        return exportMonitor.getParams().stream()
+                .filter(param -> "host".equals(param.getField()))
+                .map(ParamDTO::getValue)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .findFirst()
+                .orElse(null);
+    }
+
     protected String fileNamePrefix() {
         return "hertzbeat_monitor_" + LocalDate.now();
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
index e5cc79dd94..ff60ac8821 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
@@ -78,7 +78,10 @@ public class BulletinServiceImpl implements BulletinService {
         if (bulletin.getMonitorIds() == null || bulletin.getMonitorIds().isEmpty()) {
             throw new IllegalArgumentException("Bulletin monitorIds cannot be null or empty");
         }
-        if (bulletinDao.countByName(bulletin.getName()) > 0) {
+        int duplicateCount = bulletin.getId() == null
+                ? bulletinDao.countByName(bulletin.getName())
+                : bulletinDao.countByNameAndIdNot(bulletin.getName(), bulletin.getId());
+        if (duplicateCount > 0) {
             throw new IllegalArgumentException("Bulletin name duplicated");
         }
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
index 9d6e3a138f..6957756519 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
@@ -99,6 +99,7 @@ public class MonitorServiceImpl implements MonitorService {
     public static final String PATTERN_HTTPS = "(?i)https://";
     private static final Long MONITOR_ID_TMP = 1000000000L;
     public static final String PARAM_FIELD_PORT = "port";
+    private static final String MONITOR_COPY_SUFFIX = "_copy";
 
     @Autowired
     private ParamValidatorManager paramValidatorManager;
@@ -760,7 +761,7 @@ public class MonitorServiceImpl implements MonitorService {
         // Copy basic properties, exclude ID, jobId and status
         BeanUtils.copyProperties(sourceMonitor, newMonitor, "id", "jobId", "status");
         // Set new name
-        newMonitor.setName(sourceMonitor.getName() + "_copy");
+        newMonitor.setName(buildCopyMonitorName(sourceMonitor.getName()));
         // Set initial status
         newMonitor.setStatus(CommonConstants.MONITOR_UP_CODE);
         // Set create and update time
@@ -779,6 +780,17 @@ public class MonitorServiceImpl implements MonitorService {
         addMonitor(newMonitor, newParams, null, null);
     }
 
+    private String buildCopyMonitorName(String sourceName) {
+        String baseName = StringUtils.hasText(sourceName) ? sourceName.trim() : "monitor";
+        String candidate = baseName + MONITOR_COPY_SUFFIX;
+        int index = 2;
+        while (oldMonitorCatalogQueryService.findMonitorByName(candidate).isPresent()) {
+            candidate = baseName + MONITOR_COPY_SUFFIX + "_" + index;
+            index++;
+        }
+        return candidate;
+    }
+
     private void detectSdMonitor(Monitor monitor, List<Param> params, String collector) {
         Long monitorId = monitor.getId();
         if (monitorId == null || monitorId == 0) {
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObserveEntityServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObserveEntityServiceImpl.java
index 500f3f9a39..431307bdd5 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObserveEntityServiceImpl.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObserveEntityServiceImpl.java
@@ -18,6 +18,7 @@
 package org.apache.hertzbeat.manager.service.impl;
 
 import java.util.List;
+import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
@@ -244,4 +245,10 @@ public class ObserveEntityServiceImpl implements ObserveEntityService {
         return entityIntegrationHintService.getMonitorBindingCandidates(monitorId);
     }
 
+    @Override
+    @Transactional(readOnly = true)
+    public Map<Long, List<EntityMonitorBindingCandidate>> getMonitorBindingCandidates(List<Long> monitorIds) {
+        return entityIntegrationHintService.getMonitorBindingCandidates(monitorIds);
+    }
+
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/PluginServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/PluginServiceImpl.java
index a8206b35bf..0a4d424283 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/PluginServiceImpl.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/PluginServiceImpl.java
@@ -27,8 +27,6 @@ import java.io.OutputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Enumeration;
@@ -85,6 +83,8 @@ import org.yaml.snakeyaml.error.YAMLException;
 @RequiredArgsConstructor
 public class PluginServiceImpl implements PluginService {
 
+    private static final String PLUGIN_LIB_DIR_PROPERTY = "hertzbeat.plugin.lib.dir";
+
     private final PluginMetadataDao metadataDao;
 
     private final PluginItemDao itemDao;
@@ -196,6 +196,7 @@ public class PluginServiceImpl implements PluginService {
             return;
         }
         pluginParamDao.deletePluginParamsByPluginMetadataId(params.get(0).getPluginMetadataId());
+        pluginParamDao.flush();
         pluginParamDao.saveAll(params);
         syncPluginParamMap(params.get(0).getPluginMetadataId(), params, false);
     }
@@ -284,8 +285,8 @@ public class PluginServiceImpl implements PluginService {
     private void validateFilePath(File file) {
         try {
             String canonicalPath = file.getCanonicalPath();
-            String expectedDir = new File("plugin-lib").getCanonicalPath();
-            if (!canonicalPath.startsWith(expectedDir)) {
+            String expectedDir = resolvePluginLibDir().getCanonicalPath();
+            if (!canonicalPath.equals(expectedDir) && !canonicalPath.startsWith(expectedDir + File.separator)) {
                 throw new CommonException("File is outside the allowed directory: " + canonicalPath);
             }
         } catch (IOException e) {
@@ -315,9 +316,7 @@ public class PluginServiceImpl implements PluginService {
     @SneakyThrows
     @Transactional
     public void savePlugin(PluginUpload pluginUpload) {
-        String jarPath = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()).getAbsolutePath();
-        Path extLibPath = Paths.get(new File(jarPath).getParent(), "plugin-lib");
-        File extLibDir = extLibPath.toFile();
+        File extLibDir = resolvePluginLibDir();
         String fileName = pluginUpload.getJarFile().getOriginalFilename();
         validateFileName(fileName);
         fileName = UUID.randomUUID().toString().replace("-", "") + "_" + fileName;
@@ -365,6 +364,15 @@ public class PluginServiceImpl implements PluginService {
         }
     }
 
+    private File resolvePluginLibDir() {
+        try {
+            return new File(System.getProperty(PLUGIN_LIB_DIR_PROPERTY, "plugin-lib")).getCanonicalFile();
+        } catch (IOException e) {
+            log.error("Error resolving plugin lib dir", e);
+            throw new CommonException("Error resolving plugin lib dir: " + e.getMessage());
+        }
+    }
+
     @Override
     public boolean pluginIsEnable(Class<?> clazz) {
         return Boolean.TRUE.equals(PLUGIN_ENABLE_STATUS.get(clazz.getName()));
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/BulletinServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/BulletinServiceTest.java
index dd9768347b..d1e594d951 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/BulletinServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/BulletinServiceTest.java
@@ -105,6 +105,50 @@ public class BulletinServiceTest {
         });
     }
 
+    @Test
+    public void testValidateAllowsEditingSameBulletinName() throws Exception {
+        Map<String, List<String>> fields = new HashMap<String, List<String>>();
+        fields.put("summary", List.of("responseTime"));
+
+        List<Long> ids = new ArrayList<Long>();
+        ids.add((long) 1);
+
+        Bulletin obj = new Bulletin();
+        obj.setId((long) 7);
+        obj.setName("Ops board");
+        obj.setApp("website");
+        obj.setFields(fields);
+        obj.setMonitorIds(ids);
+
+        when(bulletinDao.countByNameAndIdNot("Ops board", 7L)).thenReturn(0);
+
+        assertDoesNotThrow(() -> {
+            bulletinService.validate(obj);
+        });
+    }
+
+    @Test
+    public void testValidateRejectsEditingToAnotherBulletinName() throws Exception {
+        Map<String, List<String>> fields = new HashMap<String, List<String>>();
+        fields.put("summary", List.of("responseTime"));
+
+        List<Long> ids = new ArrayList<Long>();
+        ids.add((long) 1);
+
+        Bulletin obj = new Bulletin();
+        obj.setId((long) 7);
+        obj.setName("Ops board");
+        obj.setApp("website");
+        obj.setFields(fields);
+        obj.setMonitorIds(ids);
+
+        when(bulletinDao.countByNameAndIdNot("Ops board", 7L)).thenReturn(1);
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            bulletinService.validate(obj);
+        });
+    }
+
     @Test
     public void testAddBulletin() throws Exception {
         Map<String, List<String>> fields = new HashMap<String, List<String>>();
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java
index 3c36a18c33..14b5581824 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java
@@ -21,14 +21,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 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.Mockito.verify;
+import static org.mockito.Mockito.when;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.Param;
+import org.apache.hertzbeat.manager.config.ManagerSseManager;
+import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
 import org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl;
 import org.apache.hertzbeat.manager.service.impl.JsonImExportServiceImpl;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
 
 /**
  * Test case for {@link JsonImExportServiceImpl}
@@ -89,4 +98,67 @@ class JsonImExportServiceTest {
         assertEquals("JSON", jsonImExportService.type());
     }
 
+    @Test
+    void importConfigRestoresInstanceFromLegacyHostParam() {
+        MonitorService monitorService = org.mockito.Mockito.mock(MonitorService.class);
+        ManagerSseManager managerSseManager = org.mockito.Mockito.mock(ManagerSseManager.class);
+        ReflectionTestUtils.setField(jsonImExportService, "monitorService", monitorService);
+        ReflectionTestUtils.setField(jsonImExportService, "managerSseManager", managerSseManager);
+        String json = """
+                [{
+                  "monitor": {
+                    "name": "Codex import monitor",
+                    "app": "website",
+                    "intervals": 60,
+                    "status": 1
+                  },
+                  "params": [
+                    {"field": "host", "type": 1, "value": "127.0.0.1"},
+                    {"field": "port", "type": 0, "value": "4223"}
+                  ]
+                }]
+                """;
+
+        jsonImExportService.importConfig("legacy-export.json", new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
+
+        ArgumentCaptor<MonitorDto> validateCaptor = ArgumentCaptor.forClass(MonitorDto.class);
+        ArgumentCaptor<Monitor> monitorCaptor = ArgumentCaptor.forClass(Monitor.class);
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<List<Param>> paramsCaptor = ArgumentCaptor.forClass(List.class);
+        verify(monitorService).validate(validateCaptor.capture(), org.mockito.Mockito.eq(false));
+        verify(monitorService).addMonitor(monitorCaptor.capture(), paramsCaptor.capture(), any(), any());
+        assertEquals("127.0.0.1", validateCaptor.getValue().getMonitor().getInstance());
+        assertEquals("127.0.0.1", monitorCaptor.getValue().getInstance());
+        assertEquals("4223", paramsCaptor.getValue().stream()
+                .filter(param -> "port".equals(param.getField()))
+                .findFirst()
+                .orElseThrow()
+                .getParamValue());
+    }
+
+    @Test
+    void exportConfigPreservesMonitorInstanceAsHost() {
+        MonitorService monitorService = org.mockito.Mockito.mock(MonitorService.class);
+        ReflectionTestUtils.setField(jsonImExportService, "monitorService", monitorService);
+        Monitor monitor = Monitor.builder()
+                .id(42L)
+                .name("Codex export monitor")
+                .app("website")
+                .instance("127.0.0.1:4223")
+                .intervals(60)
+                .status((byte) 1)
+                .build();
+        MonitorDto monitorDto = new MonitorDto();
+        monitorDto.setMonitor(monitor);
+        monitorDto.setParams(List.of(Param.builder().field("host").type((byte) 1).paramValue("127.0.0.1").build()));
+        when(monitorService.getMonitorDto(42L)).thenReturn(monitorDto);
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+        jsonImExportService.exportConfig(bos, List.of(42L));
+
+        String result = bos.toString(StandardCharsets.UTF_8);
+        assertTrue(result.contains("\"host\":\"127.0.0.1:4223\""));
+        assertTrue(result.contains("\"name\":\"Codex export monitor\""));
+    }
+
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
index bc65a9a0ef..c6819b84ee 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
@@ -76,6 +76,7 @@ import org.apache.hertzbeat.manager.service.helper.MonitorImExportHelper;
 import org.apache.hertzbeat.manager.service.impl.MonitorServiceImpl;
 import org.apache.hertzbeat.manager.support.exception.MonitorDatabaseException;
 import org.apache.hertzbeat.manager.support.exception.MonitorDetectException;
+import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.verify;
 import org.junit.jupiter.api.Assertions;
@@ -1091,6 +1092,29 @@ class MonitorServiceTest {
         assertDoesNotThrow(() -> monitorService.copyMonitor(1L));
     }
 
+    @Test
+    void copyMonitorUsesNextAvailableCopyName() {
+        Monitor monitor = Monitor.builder()
+                .intervals(1)
+                .name("memory")
+                .app("demoApp")
+                .instance("localhost")
+                .build();
+        Job job = new Job();
+        when(appService.getAppDefine(monitor.getApp())).thenReturn(job);
+        when(monitorDao.findById(1L)).thenReturn(Optional.of(monitor));
+        when(paramDao.findParamsByMonitorId(1L)).thenReturn(Collections.singletonList(new Param()));
+        when(monitorDao.findMonitorByNameEquals("memory_copy"))
+                .thenReturn(Optional.of(Monitor.builder().id(2L).name("memory_copy").build()));
+        when(monitorDao.findMonitorByNameEquals("memory_copy_2"))
+                .thenReturn(Optional.of(Monitor.builder().id(3L).name("memory_copy_2").build()));
+        when(monitorDao.findMonitorByNameEquals("memory_copy_3")).thenReturn(Optional.empty());
+
+        assertDoesNotThrow(() -> monitorService.copyMonitor(1L));
+
+        verify(monitorDao).save(argThat(saved -> "memory_copy_3".equals(saved.getName())));
+    }
+
     @Test
     void exportAll() throws Exception {
         // Create some test monitors
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 8ba96aba86..b082bf5bfc 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
@@ -309,9 +309,12 @@ class ObserveEntityServiceTest {
         entityIdentityResolutionService = new EntityIdentityResolutionService(
                 entityIdentityReadModelService, entityMonitorBindService, entityWorkspaceAccessService);
         entityIdentityWriteModelService = new EntityIdentityWriteModelService(
-                entityIdentityDao, entityIdentityResolutionService);
+                entityIdentityDao, entityIdentityResolutionService, entityWorkspaceAccessService);
         entityIntegrationHintService = new EntityIntegrationHintService(
-                entityMonitorQueryService, entityIdentityResolutionService);
+                entityMonitorQueryService,
+                entityIdentityResolutionService,
+                entityMonitorBindService,
+                entityWorkspaceAccessService);
         ReflectionTestUtils.setField(
                 observeEntityService, "entityIntegrationHintService", entityIntegrationHintService);
         entityDeletionWriteModelService = new EntityDeletionWriteModelService(
@@ -352,7 +355,11 @@ class ObserveEntityServiceTest {
         entityDefinitionDraftService = new EntityDefinitionDraftService(
                 entityDefinitionDocumentParserService,
                 entityDefinitionNormalizationService,
-                entityDefinitionMappingService);
+                entityDefinitionMappingService,
+                entityIdentityDao,
+                entityIdentityResolutionService,
+                entityWorkspaceAccessService,
+                entityMonitorBindQueryService);
         ReflectionTestUtils.setField(
                 observeEntityService, "entityDefinitionDraftService", entityDefinitionDraftService);
         entityResponseHandoffReadModelService = new EntityResponseHandoffReadModelService(entityObservabilityGateway);
@@ -396,8 +403,8 @@ class ObserveEntityServiceTest {
                 entityStatusRefreshService,
                 entitySummaryReadModelService);
         ReflectionTestUtils.setField(observeEntityService, "entityListReadModelService", entityListReadModelService);
-        lenient().when(alertSilenceDao.findAll()).thenReturn(Collections.emptyList());
-        lenient().when(alertInhibitDao.findAll()).thenReturn(Collections.emptyList());
+        lenient().when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.emptyList());
+        lenient().when(alertInhibitDao.findAlertInhibitsByEnableIsTrue()).thenReturn(Collections.emptyList());
         lenient().when(entityTraceQueryService.buildEntityTraceSummary(any()))
                 .thenReturn(new EntityTraceSummaryDto(0, 0, null, false, null));
         lenient().when(entityTraceQueryService.buildEntityTraceQueryHints(any()))
@@ -1330,9 +1337,9 @@ class ObserveEntityServiceTest {
         assertEquals(1, detail.getMonitorSummary().getAbnormalMonitors().size());
         assertEquals("otel-resource", detail.getLogSummary().getPreferredQueryType());
         assertNotNull(detail.getUnifiedEvidenceSummary());
-        assertEquals(2, detail.getUnifiedEvidenceSummary().getActiveSignalCount());
+        assertEquals(1, detail.getUnifiedEvidenceSummary().getActiveSignalCount());
         assertTrue(detail.getUnifiedEvidenceSummary().isMetricsActive());
-        assertTrue(detail.getUnifiedEvidenceSummary().isLogsActive());
+        assertFalse(detail.getUnifiedEvidenceSummary().isLogsActive());
         assertFalse(detail.getUnifiedEvidenceSummary().isTracesActive());
         assertNotNull(detail.getTriageRecommendation());
         assertEquals("metrics", detail.getTriageRecommendation().getRecommendedFocus());
@@ -1426,8 +1433,8 @@ class ObserveEntityServiceTest {
                 .equalLabels(List.of(CommonConstants.LABEL_INSTANCE))
                 .gmtUpdate(LocalDateTime.of(2026, 3, 20, 10, 5))
                 .build();
-        when(alertSilenceDao.findAll()).thenReturn(List.of(silence));
-        when(alertInhibitDao.findAll()).thenReturn(List.of(inhibit));
+        when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(List.of(silence));
+        when(alertInhibitDao.findAlertInhibitsByEnableIsTrue()).thenReturn(List.of(inhibit));
 
         EntityDetailDto detail = observeEntityService.getEntityDetail(14L);
 
@@ -1540,8 +1547,8 @@ class ObserveEntityServiceTest {
                 .equalLabels(List.of(CommonConstants.LABEL_INSTANCE))
                 .gmtUpdate(LocalDateTime.of(2026, 4, 1, 10, 25))
                 .build();
-        when(alertSilenceDao.findAll()).thenReturn(List.of(globalMatchAll, teamBetaSilence, teamAlphaSilence));
-        when(alertInhibitDao.findAll()).thenReturn(List.of(genericInhibit, teamBetaInhibit, teamAlphaInhibit));
+        when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(List.of(globalMatchAll, teamBetaSilence, teamAlphaSilence));
+        when(alertInhibitDao.findAlertInhibitsByEnableIsTrue()).thenReturn(List.of(genericInhibit, teamBetaInhibit, teamAlphaInhibit));
 
         EntityDetailDto detail = observeEntityService.getEntityDetail(141L);
 
@@ -2898,9 +2905,10 @@ class ObserveEntityServiceTest {
                 .build();
         when(observeEntityDao.findAll(any(Specification.class), any(Pageable.class)))
                 .thenReturn(new PageImpl<>(List.of(entity)));
-        when(entityIdentityDao.countByEntityId(21L)).thenReturn(0L);
-        when(entityRelationDao.countBySourceEntityIdOrTargetEntityId(21L, 21L)).thenReturn(0L);
-        when(entityMonitorBindDao.findAllByEntityIdOrderByIdAsc(21L)).thenReturn(Collections.emptyList());
+        when(entityIdentityDao.countByEntityIdInGroupByEntityId(List.of(21L))).thenReturn(Collections.emptyList());
+        when(entityMonitorBindDao.countByEntityIdInGroupByEntityId(List.of(21L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countBySourceEntityIdInGroupBySourceEntityId(List.of(21L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countByTargetEntityIdInGroupByTargetEntityId(List.of(21L))).thenReturn(Collections.emptyList());
         EntityDefinitionActivity latestActivity = EntityDefinitionActivity.builder()
                 .id(101L)
                 .entityId(21L)
@@ -2933,9 +2941,10 @@ class ObserveEntityServiceTest {
                 .build();
         when(observeEntityDao.findAll(any(Specification.class), any(Pageable.class)))
                 .thenReturn(new PageImpl<>(List.of(entity)));
-        when(entityIdentityDao.countByEntityId(23L)).thenReturn(0L);
-        when(entityRelationDao.countBySourceEntityIdOrTargetEntityId(23L, 23L)).thenReturn(0L);
-        when(entityMonitorBindDao.findAllByEntityIdOrderByIdAsc(23L)).thenReturn(Collections.emptyList());
+        when(entityIdentityDao.countByEntityIdInGroupByEntityId(List.of(23L))).thenReturn(Collections.emptyList());
+        when(entityMonitorBindDao.countByEntityIdInGroupByEntityId(List.of(23L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countBySourceEntityIdInGroupBySourceEntityId(List.of(23L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countByTargetEntityIdInGroupByTargetEntityId(List.of(23L))).thenReturn(Collections.emptyList());
         when(entityDefinitionActivityDao.findAllByEntityIdIn(any(), any(Sort.class))).thenReturn(Collections.emptyList());
 
         var page = observeEntityService.getEntities(null, null, null, null, null, null,
@@ -2970,9 +2979,10 @@ class ObserveEntityServiceTest {
                 .build();
         when(observeEntityDao.findAll(any(Specification.class), any(Pageable.class)))
                 .thenReturn(new PageImpl<>(List.of(teamAlpha, teamBeta)));
-        when(entityIdentityDao.countByEntityId(anyLong())).thenReturn(0L);
-        when(entityRelationDao.countBySourceEntityIdOrTargetEntityId(anyLong(), anyLong())).thenReturn(0L);
-        when(entityMonitorBindDao.findAllByEntityIdOrderByIdAsc(anyLong())).thenReturn(Collections.emptyList());
+        when(entityIdentityDao.countByEntityIdInGroupByEntityId(List.of(31L))).thenReturn(Collections.emptyList());
+        when(entityMonitorBindDao.countByEntityIdInGroupByEntityId(List.of(31L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countBySourceEntityIdInGroupBySourceEntityId(List.of(31L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countByTargetEntityIdInGroupByTargetEntityId(List.of(31L))).thenReturn(Collections.emptyList());
         when(entityDefinitionActivityDao.findAllByEntityIdIn(any(), any(Sort.class))).thenReturn(Collections.emptyList());
         AuthTokenRequestContext.bindWorkspaceId("team-a");
 
@@ -2996,9 +3006,10 @@ class ObserveEntityServiceTest {
                 .build();
         when(observeEntityDao.findAll(any(Specification.class), any(Pageable.class)))
                 .thenReturn(new PageImpl<>(List.of(entity)));
-        when(entityIdentityDao.countByEntityId(22L)).thenReturn(0L);
-        when(entityRelationDao.countBySourceEntityIdOrTargetEntityId(22L, 22L)).thenReturn(0L);
-        when(entityMonitorBindDao.findAllByEntityIdOrderByIdAsc(22L)).thenReturn(Collections.emptyList());
+        when(entityIdentityDao.countByEntityIdInGroupByEntityId(List.of(22L))).thenReturn(Collections.emptyList());
+        when(entityMonitorBindDao.countByEntityIdInGroupByEntityId(List.of(22L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countBySourceEntityIdInGroupBySourceEntityId(List.of(22L))).thenReturn(Collections.emptyList());
+        when(entityRelationDao.countByTargetEntityIdInGroupByTargetEntityId(List.of(22L))).thenReturn(Collections.emptyList());
         EntityDefinitionActivity discoveryActivity = EntityDefinitionActivity.builder()
                 .id(202L)
                 .entityId(22L)
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/PluginServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/PluginServiceTest.java
index 5c3181c914..78806cabbd 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/PluginServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/PluginServiceTest.java
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.manager.service;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -27,6 +28,7 @@ import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -57,6 +59,8 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.io.TempDir;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -91,7 +95,7 @@ class PluginServiceTest {
     }
 
     @Test
-    void testSavePlugin() {
+    void testSavePlugin(@TempDir File tempDir) throws IOException {
 
         List<PluginItem> pluginItems = Collections.singletonList(new PluginItem("org.apache.hertzbeat.PluginTest", PluginType.POST_ALERT));
         PluginMetadata metadata = new PluginMetadata();
@@ -99,6 +103,9 @@ class PluginServiceTest {
         metadata.setParamCount(0);
         PluginServiceImpl service = spy(pluginService);
         doReturn(metadata).when(service).validateJarFile(any());
+        File pluginLibDir = new File(tempDir, "plugin-lib");
+        String previousPluginLib = System.getProperty("hertzbeat.plugin.lib.dir");
+        System.setProperty("hertzbeat.plugin.lib.dir", pluginLibDir.getAbsolutePath());
 
         MockMultipartFile mockFile = new MockMultipartFile(
                 "file", "test-plugin.jar", "application/java-archive",
@@ -108,9 +115,23 @@ class PluginServiceTest {
         when(metadataDao.save(any(PluginMetadata.class))).thenReturn(new PluginMetadata());
         when(itemDao.saveAll(anyList())).thenReturn(Collections.emptyList());
 
-        service.savePlugin(pluginUpload);
-        verify(metadataDao, times(1)).save(any(PluginMetadata.class));
+        try {
+            service.savePlugin(pluginUpload);
+        } finally {
+            if (previousPluginLib == null) {
+                System.clearProperty("hertzbeat.plugin.lib.dir");
+            } else {
+                System.setProperty("hertzbeat.plugin.lib.dir", previousPluginLib);
+            }
+        }
+        ArgumentCaptor<File> uploadedJarCaptor = ArgumentCaptor.forClass(File.class);
+        ArgumentCaptor<PluginMetadata> metadataCaptor = ArgumentCaptor.forClass(PluginMetadata.class);
+        verify(service).validateJarFile(uploadedJarCaptor.capture());
+        verify(metadataDao, times(1)).save(metadataCaptor.capture());
         verify(itemDao, times(1)).saveAll(anyList());
+        assertTrue(uploadedJarCaptor.getValue().getCanonicalPath().startsWith(pluginLibDir.getCanonicalPath() + File.separator));
+        assertNotNull(metadataCaptor.getValue().getJarFilePath());
+        assertTrue(new File(metadataCaptor.getValue().getJarFilePath()).getCanonicalPath().startsWith(pluginLibDir.getCanonicalPath() + File.separator));
 
     }
 
@@ -168,6 +189,31 @@ class PluginServiceTest {
         verify(metadataDao, times(1)).findAll(any(Specification.class), any(PageRequest.class));
     }
 
+    @Test
+    void testSavePluginParamFlushesDeleteBeforeInsert() {
+        List<org.apache.hertzbeat.manager.pojo.dto.PluginParam> params = List.of(
+            org.apache.hertzbeat.manager.pojo.dto.PluginParam.builder()
+                .pluginMetadataId(1L)
+                .field("endpoint")
+                .paramValue("https://example.invalid/hertzbeat-plugin-audit")
+                .type((byte) 1)
+                .build(),
+            org.apache.hertzbeat.manager.pojo.dto.PluginParam.builder()
+                .pluginMetadataId(1L)
+                .field("mode")
+                .paramValue("audit-only")
+                .type((byte) 1)
+                .build()
+        );
+
+        pluginService.savePluginParam(params);
+
+        InOrder inOrder = inOrder(pluginParamDao);
+        inOrder.verify(pluginParamDao).deletePluginParamsByPluginMetadataId(1L);
+        inOrder.verify(pluginParamDao).flush();
+        inOrder.verify(pluginParamDao).saveAll(params);
+    }
+
     @Test
     void testZipSlipDetected(@TempDir File tempDir) throws Exception {
         File maliciousJar = new File(tempDir, "malicious.jar");
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentParserServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentParserServiceTest.java
index 6576c1ea8f..19d336104d 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentParserServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentParserServiceTest.java
@@ -105,6 +105,48 @@ class EntityDefinitionDocumentParserServiceTest {
         assertEquals("curl-checkout", metadataName(records.getFirst()));
     }
 
+    @Test
+    void parseDefinitionRecordsUnwrapsCurlEntityDefinitionRequestEnvelope() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        request.setFormat("curl");
+        request.setContent("""
+                curl -X POST http://127.0.0.1:1157/api/entities/import -H 'Content-Type: application/json' -d '{"format":"yaml","content":"apiVersion: hertzbeat/v1\\nkind: service\\nmetadata:\\n  name: curl-envelope-checkout\\n  namespace: commerce"}'
+                """);
+
+        List<Map<String, Object>> records = documentParserService.parseDefinitionRecords(request);
+
+        assertEquals(1, records.size());
+        assertEquals("curl-envelope-checkout", metadataName(records.getFirst()));
+    }
+
+    @Test
+    void parseDefinitionRecordsUnwrapsCurlDataRawEnvelope() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        request.setFormat("curl");
+        request.setContent("""
+                curl -X POST http://127.0.0.1:1157/api/entities/import -H 'Content-Type: application/json' --data-raw '{"format":"yaml","content":"apiVersion: hertzbeat/v1\\nkind: service\\nmetadata:\\n  name: curl-data-raw-checkout\\n  namespace: commerce"}'
+                """);
+
+        List<Map<String, Object>> records = documentParserService.parseDefinitionRecords(request);
+
+        assertEquals(1, records.size());
+        assertEquals("curl-data-raw-checkout", metadataName(records.getFirst()));
+    }
+
+    @Test
+    void parseDefinitionRecordsUnwrapsCurlDataRawEnvelopeWithEquals() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        request.setFormat("curl");
+        request.setContent("""
+                curl -X POST http://127.0.0.1:1157/api/entities/import -H 'Content-Type: application/json' --data-raw='{"format":"yaml","content":"apiVersion: hertzbeat/v1\\nkind: service\\nmetadata:\\n  name: curl-data-raw-equals-checkout\\n  namespace: commerce"}'
+                """);
+
+        List<Map<String, Object>> records = documentParserService.parseDefinitionRecords(request);
+
+        assertEquals(1, records.size());
+        assertEquals("curl-data-raw-equals-checkout", metadataName(records.getFirst()));
+    }
+
     @Test
     void parseDefinitionRecordsRejectsBlankContent() {
         EntityDefinitionRequest request = new EntityDefinitionRequest();
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDraftServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDraftServiceTest.java
index 90a6b0a94d..4fc1de75e7 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDraftServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDraftServiceTest.java
@@ -26,6 +26,11 @@ import static org.mockito.Mockito.when;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
 import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
 import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionRequest;
 import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
@@ -54,6 +59,18 @@ class EntityDefinitionDraftServiceTest {
     @Mock
     private EntityDefinitionMappingService entityDefinitionMappingService;
 
+    @Mock
+    private EntityIdentityDao entityIdentityDao;
+
+    @Mock
+    private EntityIdentityResolutionService entityIdentityResolutionService;
+
+    @Mock
+    private EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    @Mock
+    private EntityMonitorBindQueryService entityMonitorBindQueryService;
+
     @Test
     void parseEntityDefinitionMapsSingleNormalizedDraftWithEntityAndWorkspaceContext() {
         EntityDefinitionRequest request = new EntityDefinitionRequest();
@@ -131,4 +148,100 @@ class EntityDefinitionDraftServiceTest {
 
         assertEquals(List.of(firstDto, secondDto), entityDtos);
     }
+
+    @Test
+    void parseEntityDefinitionBundleRejectsAccessiblePrimaryIdentityCollisionsBeforeWrite() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        request.setContent("kind: service");
+        Map<String, Object> rawRecord = Map.of("kind", "service");
+        EntityDefinition normalizedDefinition = new EntityDefinition();
+        EntityDto dto = new EntityDto();
+        dto.setIdentities(List.of(EntityIdentity.builder()
+                .identityKey("service.name")
+                .identityValue("checkout-api")
+                .primaryIdentity(true)
+                .build()));
+        EntityIdentity persistedIdentity = EntityIdentity.builder()
+                .entityId(42L)
+                .identityKey("service.name")
+                .identityValue("checkout-api")
+                .normalizedValue("checkout-api")
+                .primaryIdentity(true)
+                .build();
+        ObserveEntity visibleEntity = ObserveEntity.builder()
+                .id(42L)
+                .name("checkout-api")
+                .type("service")
+                .build();
+        when(entityDefinitionDocumentParserService.parseDefinitionRecords(request)).thenReturn(List.of(rawRecord));
+        when(entityDefinitionNormalizationService.normalizeDefinition(rawRecord)).thenReturn(normalizedDefinition);
+        when(entityDefinitionMappingService.toEntityDto(normalizedDefinition, null, "team-a")).thenReturn(dto);
+        when(entityIdentityResolutionService.normalizeIdentityValue("service.name", "checkout-api"))
+                .thenReturn("checkout-api");
+        when(entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(
+                Set.of("service.name"), Set.of("checkout-api"))).thenReturn(List.of(persistedIdentity));
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(Set.of(42L)))
+                .thenReturn(List.of(visibleEntity));
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> definitionDraftService.parseEntityDefinitionBundle(request, "team-a"));
+
+        assertEquals("Entity primary identity already exists: service.name=checkout-api.", exception.getMessage());
+    }
+
+    @Test
+    void parseEntityDefinitionAllowsCurrentEntityPrimaryIdentityWhenEditingDefinition() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        request.setContent("kind: service");
+        Map<String, Object> rawRecord = Map.of("kind", "service");
+        EntityDefinition normalizedDefinition = new EntityDefinition();
+        EntityDto dto = new EntityDto();
+        dto.setIdentities(List.of(EntityIdentity.builder()
+                .identityKey("service.name")
+                .identityValue("checkout-api")
+                .primaryIdentity(true)
+                .build()));
+        EntityIdentity currentIdentity = EntityIdentity.builder()
+                .entityId(42L)
+                .identityKey("service.name")
+                .identityValue("checkout-api")
+                .normalizedValue("checkout-api")
+                .primaryIdentity(true)
+                .build();
+        when(entityDefinitionDocumentParserService.parseDefinitionRecords(request)).thenReturn(List.of(rawRecord));
+        when(entityDefinitionNormalizationService.normalizeDefinition(rawRecord)).thenReturn(normalizedDefinition);
+        when(entityDefinitionMappingService.toEntityDto(normalizedDefinition, 42L, "team-a")).thenReturn(dto);
+        when(entityIdentityResolutionService.normalizeIdentityValue("service.name", "checkout-api"))
+                .thenReturn("checkout-api");
+        when(entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(
+                Set.of("service.name"), Set.of("checkout-api"))).thenReturn(List.of(currentIdentity));
+
+        EntityDto actualDto = definitionDraftService.parseEntityDefinition(request, 42L, "team-a");
+
+        assertSame(dto, actualDto);
+    }
+
+    @Test
+    void parseEntityDefinitionBundleRejectsMonitorBindAlreadyAssignedBeforeWrite() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        request.setContent("kind: service");
+        Map<String, Object> rawRecord = Map.of("kind", "service");
+        EntityDefinition normalizedDefinition = new EntityDefinition();
+        EntityDto dto = new EntityDto();
+        dto.setMonitorBinds(List.of(EntityMonitorBind.builder()
+                .monitorId(501L)
+                .build()));
+        when(entityDefinitionDocumentParserService.parseDefinitionRecords(request)).thenReturn(List.of(rawRecord));
+        when(entityDefinitionNormalizationService.normalizeDefinition(rawRecord)).thenReturn(normalizedDefinition);
+        when(entityDefinitionMappingService.toEntityDto(normalizedDefinition, null, "team-a")).thenReturn(dto);
+        when(entityMonitorBindQueryService.findMonitorBindsByMonitorId(501L)).thenReturn(List.of(EntityMonitorBind.builder()
+                .entityId(302L)
+                .monitorId(501L)
+                .build()));
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> definitionDraftService.parseEntityDefinitionBundle(request, "team-a"));
+
+        assertEquals("Monitor already bound to another entity: 501.", exception.getMessage());
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionNormalizationServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionNormalizationServiceTest.java
index b2c2f4dea8..3618d1cbbf 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionNormalizationServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionNormalizationServiceTest.java
@@ -18,6 +18,7 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.when;
 
 import java.util.List;
@@ -141,4 +142,17 @@ class EntityDefinitionNormalizationServiceTest {
         assertEquals("level:error", definition.getHertzbeat().getLogs().getFirst().getQuery());
         assertEquals(List.of("checkout", "api"), definition.getHertzbeat().getPerformanceData().getTags());
     }
+
+    @Test
+    void normalizeDefinitionRejectsUnsupportedExplicitKindInsteadOfDefaultingToService() {
+        Map<String, Object> record = Map.of(
+                "apiVersion", "hertzbeat/v1",
+                "kind", "cache",
+                "metadata", Map.of("name", "redis-cache"));
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> normalizationService.normalizeDefinition(record));
+
+        assertEquals("Unsupported entity definition kind: cache.", exception.getMessage());
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionRelationNormalizationServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionRelationNormalizationServiceTest.java
index f48e8b6d7a..0d6e408167 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionRelationNormalizationServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionRelationNormalizationServiceTest.java
@@ -104,6 +104,29 @@ class EntityDefinitionRelationNormalizationServiceTest {
         assertEquals(List.of("service:commerce/orders-worker", "database:commerce/orders-db"), references);
     }
 
+    @Test
+    void dedupesDependsOnWhenExplicitRelationAlreadyNamesSameTargetReference() {
+        EntityDefinitionRelationNormalizationService normalizationService =
+                new EntityDefinitionRelationNormalizationService(entityRelationService);
+        Map<String, Object> specMap = Map.of(
+                "relations", List.of(Map.of(
+                        "targetEntityId", "42",
+                        "targetRef", "service:commerce/payments-api",
+                        "relationType", "depends_on",
+                        "relationSource", "definition")),
+                "dependsOn", List.of("service:commerce/payments-api")
+        );
+
+        List<EntityDefinition.Relation> relations = normalizationService.mergeDefinitionRelations(
+                normalizationService.extractDefinitionRelations("relations", specMap),
+                normalizationService.extractDefinitionDependsOn(specMap.get("dependsOn")));
+
+        assertEquals(1, relations.size());
+        assertEquals(42L, relations.getFirst().getTargetEntityId());
+        assertEquals("service:commerce/payments-api", relations.getFirst().getTargetRef());
+        assertEquals("definition", relations.getFirst().getRelationSource());
+    }
+
     @Test
     void attachesRelationEnvelopeFromCanonicalAndLegacyFields() {
         EntityDefinitionRelationNormalizationService normalizationService =
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTypeResolverServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTypeResolverServiceTest.java
index a55bcf7bbb..a1212ab120 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTypeResolverServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTypeResolverServiceTest.java
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.Map;
 import org.junit.jupiter.api.Test;
@@ -57,4 +58,15 @@ class EntityDefinitionTypeResolverServiceTest {
         assertEquals("mysql", resolverService.resolveDefinitionSubtype(
                 Map.of("resourceType", "mysql"), Map.of("type", "database"), "database"));
     }
+
+    @Test
+    void rejectsUnsupportedExplicitKindOrEntityTypeInsteadOfDefaultingToService() {
+        IllegalArgumentException kind = assertThrows(IllegalArgumentException.class,
+                () -> resolverService.resolveDefinitionEntityType(Map.of("kind", "cache"), Map.of()));
+        assertEquals("Unsupported entity definition kind: cache.", kind.getMessage());
+
+        IllegalArgumentException entityType = assertThrows(IllegalArgumentException.class,
+                () -> resolverService.resolveDefinitionEntityType(Map.of(), Map.of("entity_type", "cache")));
+        assertEquals("Unsupported entity definition entity type: cache.", entityType.getMessage());
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailChildEvidenceSourceOwnershipTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailChildEvidenceSourceOwnershipTest.java
index 0c66f0ad55..1a079d5c62 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailChildEvidenceSourceOwnershipTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailChildEvidenceSourceOwnershipTest.java
@@ -56,13 +56,18 @@ class EntityDetailChildEvidenceSourceOwnershipTest {
         assertTrue(detailSource.contains("private final EntityRelationService entityRelationService"));
         assertTrue(detailSource.contains("entityMonitorBindService.findMonitorBinds(entityId)"));
         assertTrue(detailSource.contains("entityRelationService.findEntityRelations(entityId)"));
+        assertTrue(detailSource.contains("entityRelationService.findEntityRelations(entityId, relationPreviewLimit)"));
+        assertTrue(detailSource.contains("entityRelationService.countEntityRelations(entityId)"));
 
         assertTrue(monitorBindSource.contains("public List<EntityMonitorBind> findMonitorBinds(Long entityId)"));
         assertTrue(monitorBindSource.contains("entityMonitorBindQueryService.findMonitorBinds(entityId)"));
         assertTrue(monitorBindQuerySource.contains("entityMonitorBindDao.findAllByEntityIdOrderByIdAsc(entityId)"));
         assertTrue(relationSource.contains("public List<EntityRelation> findEntityRelations(Long entityId)"));
+        assertTrue(relationSource.contains("public List<EntityRelation> findEntityRelations(Long entityId, int limit)"));
         assertTrue(relationSource.contains("entityRelationQueryService.findEntityRelations(entityId)"));
+        assertTrue(relationSource.contains("entityRelationQueryService.findEntityRelations(entityId, limit)"));
         assertTrue(relationQuerySource.contains(
                 "entityRelationDao.findBySourceEntityIdOrTargetEntityId(entityId, entityId)"));
+        assertTrue(relationQuerySource.contains("PageRequest.of(0, limit"));
     }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailFacadeHandoffSourceOwnershipTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailFacadeHandoffSourceOwnershipTest.java
index 9c5de8ac9f..63fcb076b9 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailFacadeHandoffSourceOwnershipTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailFacadeHandoffSourceOwnershipTest.java
@@ -49,7 +49,12 @@ class EntityDetailFacadeHandoffSourceOwnershipTest {
                 "Detail observability read model should own the entity DTO loading handoff");
         assertTrue(detailObservabilitySource.contains("public EntityDetailDto buildEntityDetail(long entityId)"),
                 "Detail observability read model should expose an id-based detail assembly handoff");
-        assertTrue(detailObservabilitySource.contains("entityDetailReadModelService.loadEntityDto(entityId)"),
-                "Detail observability read model should load the entity DTO before runtime evidence assembly");
+        assertTrue(detailObservabilitySource.contains("ENTITY_DETAIL_RELATION_PREVIEW_LIMIT"),
+                "Detail observability read model should cap relation preview evidence for large graph payloads");
+        assertTrue(detailObservabilitySource.contains(
+                        "entityDetailReadModelService.loadEntityDto(\n                entityId, ENTITY_DETAIL_RELATION_PREVIEW_LIMIT)"),
+                "Detail observability read model should load a relation preview before runtime evidence assembly");
+        assertTrue(detailObservabilitySource.contains("entityDetailReadModelService.countEntityRelations(entityId)"),
+                "Detail observability read model should count full relations separately from preview rows");
     }
 }
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 9553cbc5e6..1135ec19c0 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
@@ -32,6 +32,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.IntStream;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.common.entity.manager.EntityRelation;
@@ -277,13 +278,101 @@ class EntityDetailObservabilityReadModelServiceTest {
                 same(opsSummary));
     }
 
+    @Test
+    void buildEntityDetailCapsBoundMonitorPreviewWithoutTruncatingEvidenceInputs() {
+        ObserveEntity entity = ObserveEntity.builder()
+                .id(502L)
+                .name("checkout-api")
+                .type("service")
+                .workspaceId("team-a")
+                .build();
+        EntityDto entityDto = new EntityDto();
+        entityDto.setEntity(entity);
+        entityDto.setIdentities(Collections.emptyList());
+        List<Monitor> monitors = IntStream.rangeClosed(1, 60)
+                .mapToObj(index -> Monitor.builder()
+                        .id(700L + index)
+                        .app("springboot3")
+                        .name("checkout monitor " + index)
+                        .status((byte) 2)
+                        .build())
+                .toList();
+        List<SingleAlert> activeAlerts = Collections.emptyList();
+        EntityStatusInfo statusInfo = new EntityStatusInfo(
+                "available", "all monitors are up", 60, 60, 0, 0, 0, LocalDateTime.now());
+        EntityEvidenceSummaryInfo evidenceSummary = new EntityEvidenceSummaryInfo(60, 0, 0, 0, 0, 123L);
+        EntityAlertSummaryInfo alertSummary = new EntityAlertSummaryInfo(
+                0, Collections.emptyList(), Collections.emptyMap(), 123L);
+        EntityMonitorSummaryInfo monitorSummary = new EntityMonitorSummaryInfo(
+                60, Map.of("springboot3", 60L), Map.of("up", 60L), Collections.emptyList(), 123L);
+        EntityLogSummaryInfo logSummary = new EntityLogSummaryInfo(
+                0, "none", null, Collections.emptyMap(), Collections.emptyList(), null);
+        EntityTraceSummaryDto traceSummary = new EntityTraceSummaryDto(0, 0, null, false, null);
+        EntityUnifiedEvidenceSummary unifiedSummary = new EntityUnifiedEvidenceSummary(
+                60, true, false, false, 60, 0, 0, 123L, Collections.emptyList());
+        EntityTriageRecommendation triage = new EntityTriageRecommendation(
+                "stable", "metrics", "Review monitors", "No active alert", null, "Open monitors", 123L);
+        EntityObservabilityDetailBundle detailBundle = new EntityObservabilityDetailBundle(
+                traceSummary, Collections.emptyList(), logSummary, Collections.emptyList(),
+                Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), unifiedSummary, triage);
+        EntityOpsSummaryInfo opsSummary = new EntityOpsSummaryInfo(true, true, false, false, true, 80);
+        EntityStatusPageSummaryInfo statusPageSummary = new EntityStatusPageSummaryInfo(true, 0, 123L, false);
+        EntityResponseHandoffsInfo handoffs = new EntityResponseHandoffsInfo();
+        EntityDetailDto.EntityNoiseControlSummaryInfo noiseSummary =
+                new EntityDetailDto.EntityNoiseControlSummaryInfo(0, 0, Collections.emptyList(),
+                        Collections.emptyList(), false);
+
+        EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence =
+                new EntityStatusRefreshService.EntityRuntimeStatusEvidence(monitors, activeAlerts, statusInfo);
+        when(entityStatusRefreshService.refreshEntityStatusWithEvidence(entity, "team-a")).thenReturn(runtimeEvidence);
+        when(entityObservabilityGateway.buildEntityLogQueryHints(Collections.emptyList(), monitors))
+                .thenReturn(Collections.emptyList());
+        when(entityObservabilityGateway.buildEntityEvidenceSummary(
+                entity, statusInfo, 0L, 0, monitors, activeAlerts)).thenReturn(evidenceSummary);
+        when(entityObservabilityGateway.buildEntityAlertSummary(activeAlerts)).thenReturn(alertSummary);
+        when(entityObservabilityGateway.buildEntityMonitorSummary(monitors)).thenReturn(monitorSummary);
+        when(entityObservabilityGateway.buildEntityLogSummary(Collections.emptyList())).thenReturn(logSummary);
+        when(entityObservabilityGateway.resolveEntityDetailBundle(
+                any(ObservedEntityContext.class), same(statusInfo), same(evidenceSummary), same(monitorSummary),
+                same(logSummary), same(monitors), eq(Collections.emptyList()))).thenReturn(detailBundle);
+        when(entityObservabilityGateway.buildEntityOpsSummary(entity, 0L, evidenceSummary)).thenReturn(opsSummary);
+        when(entityObservabilityGateway.buildEntityNextActions(entity, evidenceSummary, logSummary, opsSummary))
+                .thenReturn(Collections.emptyList());
+        when(entityObservabilityGateway.buildEntityStatusPageSummary(entity, opsSummary)).thenReturn(statusPageSummary);
+        when(entityResponseHandoffReadModelService.buildResponseHandoffs(
+                eq(502L), any(ObservedEntityContext.class), same(activeAlerts), same(monitors),
+                any(EntitySignalEvidenceBundle.class), same(opsSummary))).thenReturn(handoffs);
+        when(entityNoiseControlReadModelService.buildNoiseControlSummary(
+                entityDto, monitors, activeAlerts, "team-a")).thenReturn(noiseSummary);
+        when(entityActivityReadModelService.getDefinitionActivities(502L, 12, "team-a"))
+                .thenReturn(Collections.emptyList());
+
+        EntityDetailDto detail = entityDetailObservabilityReadModelService.buildEntityDetail(entityDto, "team-a");
+
+        assertEquals(EntityDetailObservabilityReadModelService.ENTITY_DETAIL_BOUND_MONITOR_PREVIEW_LIMIT,
+                detail.getBoundMonitors().size());
+        assertEquals(701L, detail.getBoundMonitors().getFirst().getId());
+        assertEquals(750L, detail.getBoundMonitors().getLast().getId());
+        assertSame(monitorSummary, detail.getMonitorSummary());
+        verify(entityObservabilityGateway).buildEntityMonitorSummary(same(monitors));
+        verify(entityObservabilityGateway).resolveEntityDetailBundle(
+                any(ObservedEntityContext.class), same(statusInfo), same(evidenceSummary), same(monitorSummary),
+                same(logSummary), same(monitors), eq(Collections.emptyList()));
+        verify(entityResponseHandoffReadModelService).buildResponseHandoffs(
+                eq(502L), any(ObservedEntityContext.class), same(activeAlerts), same(monitors),
+                any(EntitySignalEvidenceBundle.class), same(opsSummary));
+    }
+
     @Test
     void buildEntityDetailByIdReturnsNullWhenEntityReadModelHasNoAccessibleEntity() {
-        when(entityDetailReadModelService.loadEntityDto(501L)).thenReturn(null);
+        when(entityDetailReadModelService.loadEntityDto(
+                501L, EntityDetailObservabilityReadModelService.ENTITY_DETAIL_RELATION_PREVIEW_LIMIT))
+                .thenReturn(null);
 
         assertNull(entityDetailObservabilityReadModelService.buildEntityDetail(501L));
 
-        verify(entityDetailReadModelService).loadEntityDto(501L);
+        verify(entityDetailReadModelService).loadEntityDto(
+                501L, EntityDetailObservabilityReadModelService.ENTITY_DETAIL_RELATION_PREVIEW_LIMIT);
         verifyNoInteractions(entityStatusRefreshService, entityObservabilityGateway,
                 entityResponseHandoffReadModelService, entityNoiseControlReadModelService,
                 entityActivityReadModelService, entityWorkspaceAccessService);
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailReadModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailReadModelServiceTest.java
index 5d34ca11b2..c4dc750a1b 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailReadModelServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailReadModelServiceTest.java
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
@@ -123,6 +124,38 @@ class EntityDetailReadModelServiceTest {
         assertEquals(List.of(), dto.getRelations());
     }
 
+    @Test
+    void loadEntityDtoWithRelationPreviewLimitKeepsDefinitionLoadsSeparateFromDetailPreviewLoads() {
+        ObserveEntity entity = ObserveEntity.builder()
+                .id(807L)
+                .name("checkout-graph")
+                .workspaceId("team-a")
+                .build();
+        EntityRelation relation = EntityRelation.builder()
+                .id(907L)
+                .sourceEntityId(807L)
+                .targetEntityId(808L)
+                .relationType("calls")
+                .build();
+        when(entityWorkspaceAccessService.findAccessibleEntityForRequestWorkspace(807L))
+                .thenReturn(Optional.of(entity));
+        when(entityIdentityReadModelService.findIdentities(807L)).thenReturn(List.of());
+        when(entityMonitorBindService.findMonitorBinds(807L)).thenReturn(List.of());
+        when(entityRelationService.findEntityRelations(807L, 50)).thenReturn(List.of(relation));
+
+        EntityDto dto = entityDetailReadModelService.loadEntityDto(807L, 50);
+
+        assertEquals(List.of(relation), dto.getRelations());
+        verify(entityRelationService).findEntityRelations(807L, 50);
+    }
+
+    @Test
+    void countEntityRelationsDelegatesToRelationBoundary() {
+        when(entityRelationService.countEntityRelations(807L)).thenReturn(99L);
+
+        assertEquals(99L, entityDetailReadModelService.countEntityRelations(807L));
+    }
+
     @Test
     void loadEntityDtoReturnsNullBeforeChildLookupsWhenWorkspaceRejectsEntity() {
         when(entityWorkspaceAccessService.findAccessibleEntityById(811L, "team-a"))
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityQueryServiceTest.java
index 5c77f17bd6..4ebf72f774 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityQueryServiceTest.java
@@ -22,6 +22,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
@@ -71,6 +72,16 @@ class EntityIdentityQueryServiceTest {
         verify(entityIdentityDao).countByEntityId(801L);
     }
 
+    @Test
+    void countIdentitiesByEntityIdsBatchesCurrentPageRows() {
+        List<Long> entityIds = List.of(801L, 802L);
+        when(entityIdentityDao.countByEntityIdInGroupByEntityId(entityIds))
+                .thenReturn(List.<Object[]>of(new Object[] {801L, 2L}, new Object[] {802L, 1L}));
+
+        assertEquals(Map.of(801L, 2L, 802L, 1L), entityIdentityQueryService.countIdentitiesByEntityIds(entityIds));
+        verify(entityIdentityDao).countByEntityIdInGroupByEntityId(entityIds);
+    }
+
     @Test
     void findMatchingIdentitiesUsesPersistedKeysAndNormalizedValues() {
         EntityIdentity serviceIdentity = EntityIdentity.builder()
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityWriteModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityWriteModelServiceTest.java
index 159f49f536..2a815da537 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityWriteModelServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityWriteModelServiceTest.java
@@ -18,8 +18,10 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.anySet;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.verify;
@@ -28,6 +30,7 @@ import static org.mockito.Mockito.when;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+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;
@@ -53,10 +56,14 @@ class EntityIdentityWriteModelServiceTest {
     @Mock
     private EntityIdentityResolutionService entityIdentityResolutionService;
 
+    @Mock
+    private EntityWorkspaceAccessService entityWorkspaceAccessService;
+
     @BeforeEach
     void setUp() {
         identityWriteModelService =
-                new EntityIdentityWriteModelService(entityIdentityDao, entityIdentityResolutionService);
+                new EntityIdentityWriteModelService(entityIdentityDao, entityIdentityResolutionService,
+                        entityWorkspaceAccessService);
         when(entityIdentityResolutionService.normalizeIdentityValue(anyString(), anyString()))
                 .thenAnswer(invocation -> invocation.getArgument(1, String.class).trim().toLowerCase(Locale.ROOT));
         when(entityIdentityResolutionService.defaultIdentityPriority(anyString())).thenReturn(40);
@@ -64,6 +71,8 @@ class EntityIdentityWriteModelServiceTest {
         lenient().when(entityIdentityResolutionService.defaultIdentityPriority("service.namespace")).thenReturn(30);
         lenient().when(entityIdentityResolutionService.defaultIdentityPriority("deployment.environment.name")).thenReturn(20);
         lenient().when(entityIdentityResolutionService.defaultIdentityPriority("messaging.destination.name")).thenReturn(120);
+        lenient().when(entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(anySet(), anySet()))
+                .thenReturn(Collections.emptyList());
     }
 
     @Test
@@ -103,6 +112,8 @@ class EntityIdentityWriteModelServiceTest {
         InOrder inOrder = inOrder(entityIdentityDao);
         inOrder.verify(entityIdentityDao).deleteAllByEntityId(7L);
         inOrder.verify(entityIdentityDao).flush();
+        inOrder.verify(entityIdentityDao).findAllByIdentityKeyInAndNormalizedValueIn(
+                Set.of("service.name"), Set.of("checkout-api"));
 
         @SuppressWarnings("unchecked")
         ArgumentCaptor<List<EntityIdentity>> identitiesCaptor = ArgumentCaptor.forClass(List.class);
@@ -199,4 +210,72 @@ class EntityIdentityWriteModelServiceTest {
         assertEquals("prod", environmentIdentity.getIdentityValue());
         assertEquals(20, environmentIdentity.getPriority());
     }
+
+    @Test
+    void replaceIdentitiesRejectsAccessiblePrimaryIdentityCollision() {
+        ObserveEntity entity = ObserveEntity.builder()
+                .id(9L)
+                .type("service")
+                .name("catalog-b")
+                .workspaceId("team-a")
+                .build();
+        EntityIdentity sharedPrimary = EntityIdentity.builder()
+                .identityType("manual")
+                .identityKey("service.name")
+                .identityValue("shared-checkout")
+                .primaryIdentity(true)
+                .build();
+        EntityIdentity existing = EntityIdentity.builder()
+                .entityId(8L)
+                .identityKey("service.name")
+                .identityValue("shared-checkout")
+                .normalizedValue("shared-checkout")
+                .primaryIdentity(true)
+                .build();
+        when(entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(
+                Set.of("service.name"), Set.of("shared-checkout")))
+                .thenReturn(List.of(existing));
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(Set.of(8L)))
+                .thenReturn(List.of(ObserveEntity.builder().id(8L).name("catalog-a").build()));
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> identityWriteModelService.replaceIdentities(entity, List.of(sharedPrimary)));
+
+        assertEquals("Entity primary identity already exists: service.name=shared-checkout.", thrown.getMessage());
+    }
+
+    @Test
+    void replaceIdentitiesAllowsSharedNonPrimaryContextIdentities() {
+        ObserveEntity entity = ObserveEntity.builder()
+                .id(10L)
+                .type("service")
+                .name("catalog-c")
+                .workspaceId("team-a")
+                .build();
+        EntityIdentity primary = EntityIdentity.builder()
+                .identityType("manual")
+                .identityKey("service.name")
+                .identityValue("catalog-c")
+                .primaryIdentity(true)
+                .build();
+        EntityIdentity sharedNamespace = EntityIdentity.builder()
+                .identityType("manual")
+                .identityKey("service.namespace")
+                .identityValue("shared-namespace")
+                .primaryIdentity(false)
+                .build();
+        when(entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(
+                Set.of("service.name"), Set.of("catalog-c")))
+                .thenReturn(Collections.emptyList());
+
+        identityWriteModelService.replaceIdentities(entity, List.of(primary, sharedNamespace));
+
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<List<EntityIdentity>> identitiesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(entityIdentityDao).saveAll(identitiesCaptor.capture());
+        List<EntityIdentity> rows = identitiesCaptor.getValue();
+        assertEquals(2, rows.size());
+        assertEquals("service.namespace", rows.get(1).getIdentityKey());
+        assertEquals("shared-namespace", rows.get(1).getIdentityValue());
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIntegrationHintServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIntegrationHintServiceTest.java
index 841552b2c4..ac83eb6f74 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIntegrationHintServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityIntegrationHintServiceTest.java
@@ -23,10 +23,13 @@ import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+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.manager.pojo.dto.EntityMonitorBindingCandidate;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -49,10 +52,19 @@ class EntityIntegrationHintServiceTest {
     @Mock
     private EntityIdentityResolutionService entityIdentityResolutionService;
 
+    @Mock
+    private EntityMonitorBindService entityMonitorBindService;
+
+    @Mock
+    private EntityWorkspaceAccessService entityWorkspaceAccessService;
+
     @BeforeEach
     void setUp() {
         integrationHintService = new EntityIntegrationHintService(
-                entityMonitorQueryService, entityIdentityResolutionService);
+                entityMonitorQueryService,
+                entityIdentityResolutionService,
+                entityMonitorBindService,
+                entityWorkspaceAccessService);
     }
 
     @Test
@@ -64,6 +76,8 @@ class EntityIntegrationHintServiceTest {
 
         assertTrue(candidates.isEmpty());
         verifyNoInteractions(entityIdentityResolutionService);
+        verifyNoInteractions(entityMonitorBindService);
+        verifyNoInteractions(entityWorkspaceAccessService);
     }
 
     @Test
@@ -84,13 +98,76 @@ class EntityIntegrationHintServiceTest {
         when(entityMonitorQueryService.findMonitor(502L)).thenReturn(Optional.of(monitor));
         when(entityIdentityResolutionService.resolveMonitorBindingCandidates(monitor))
                 .thenReturn(List.of(candidate));
+        when(entityMonitorBindService.findMonitorBindsByMonitorId(502L)).thenReturn(List.of());
 
         List<EntityMonitorBindingCandidate> candidates =
                 integrationHintService.getMonitorBindingCandidates(502L);
 
         assertEquals(List.of(candidate), candidates);
-        InOrder inOrder = inOrder(entityMonitorQueryService, entityIdentityResolutionService);
+        InOrder inOrder = inOrder(entityMonitorQueryService, entityIdentityResolutionService, entityMonitorBindService);
         inOrder.verify(entityMonitorQueryService).findMonitor(502L);
         inOrder.verify(entityIdentityResolutionService).resolveMonitorBindingCandidates(monitor);
+        inOrder.verify(entityMonitorBindService).findMonitorBindsByMonitorId(502L);
+    }
+
+    @Test
+    void getMonitorBindingCandidatesIncludesExistingBoundEntityWithoutIdentityMatch() {
+        Monitor monitor = new Monitor();
+        monitor.setId(503L);
+        monitor.setApp("website");
+        monitor.setName("checkout-probe");
+        when(entityMonitorQueryService.findMonitor(503L)).thenReturn(Optional.of(monitor));
+        when(entityIdentityResolutionService.resolveMonitorBindingCandidates(monitor)).thenReturn(List.of());
+        when(entityMonitorBindService.findMonitorBindsByMonitorId(503L)).thenReturn(List.of(
+                EntityMonitorBind.builder().entityId(91L).monitorId(503L).bindType("manual").build()
+        ));
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(java.util.Set.of(91L)))
+                .thenReturn(List.of(ObserveEntity.builder()
+                        .id(91L)
+                        .type("service")
+                        .name("checkout-api")
+                        .displayName("Checkout API")
+                        .build()));
+
+        List<EntityMonitorBindingCandidate> candidates =
+                integrationHintService.getMonitorBindingCandidates(503L);
+
+        assertEquals(1, candidates.size());
+        EntityMonitorBindingCandidate candidate = candidates.getFirst();
+        assertEquals(91L, candidate.getEntityId());
+        assertEquals("Checkout API", candidate.getEntityName());
+        assertEquals("service", candidate.getEntityType());
+        assertEquals(0, candidate.getScore());
+        assertEquals("already_bound", candidate.getRecommendation());
+        assertTrue(candidate.isAlreadyBound());
+        assertEquals(Map.of(), candidate.getMatchedIdentities());
+    }
+
+    @Test
+    void getMonitorBindingCandidatesBatchesDistinctMonitorIdsInOrder() {
+        Monitor monitor502 = new Monitor();
+        monitor502.setId(502L);
+        monitor502.setApp("springboot3");
+        monitor502.setName("checkout-api");
+        EntityMonitorBindingCandidate candidate = new EntityMonitorBindingCandidate(
+                90L,
+                "Checkout API",
+                "service",
+                120,
+                "direct",
+                false,
+                Map.of("service.name", List.of("checkout-api")));
+        when(entityMonitorQueryService.findMonitor(502L)).thenReturn(Optional.of(monitor502));
+        when(entityIdentityResolutionService.resolveMonitorBindingCandidates(monitor502))
+                .thenReturn(List.of(candidate));
+        when(entityMonitorBindService.findMonitorBindsByMonitorId(502L)).thenReturn(List.of());
+        when(entityMonitorQueryService.findMonitor(503L)).thenReturn(Optional.empty());
+
+        Map<Long, List<EntityMonitorBindingCandidate>> candidates =
+                integrationHintService.getMonitorBindingCandidates(Arrays.asList(502L, 503L, 502L, null, -1L));
+
+        assertEquals(List.of(502L, 503L), List.copyOf(candidates.keySet()));
+        assertEquals(List.of(candidate), candidates.get(502L));
+        assertTrue(candidates.get(503L).isEmpty());
     }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListCatalogQuerySourceOwnershipTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListCatalogQuerySourceOwnershipTest.java
index fcaad79ea2..6b72ed0cbe 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListCatalogQuerySourceOwnershipTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListCatalogQuerySourceOwnershipTest.java
@@ -69,6 +69,21 @@ class EntityListCatalogQuerySourceOwnershipTest {
         assertTrue(querySource.contains("entityWorkspaceQueryService.findEntityPage("));
         assertTrue(querySource.contains("buildEntitySpecification"));
         assertTrue(querySource.contains("buildSearchPredicates"));
+        assertTrue(querySource.contains("for (String normalizedSearch : normalizeSearchTextVariants(search))"),
+                "Catalog search should normalize exact copied endpoint names before predicate construction");
+        assertTrue(querySource.contains("URLDecoder.decode(trimmedSearch, StandardCharsets.UTF_8)"),
+                "Catalog search should tolerate URL-encoded copied endpoint names from the BFF boundary");
+        assertTrue(querySource.contains("normalizedSearch.replace(\"localhost\", \"127.0.0.1\")"),
+                "Catalog search should treat localhost and loopback endpoint names as equivalent");
+        assertTrue(querySource.contains("criteriaBuilder.equal(criteriaBuilder.lower(root.get(\"name\")), normalizedSearch)"),
+                "Catalog search should exact-match entity names such as copied endpoint IP:port values");
+        assertTrue(querySource.contains(
+                "criteriaBuilder.equal(criteriaBuilder.lower(root.get(\"displayName\")), normalizedSearch)"),
+                "Catalog search should exact-match display names before falling back to fuzzy LIKE");
+        assertTrue(querySource.contains("normalizedSearch.split(\"[^a-z0-9]+\")"),
+                "Catalog search should tokenize copied endpoint names such as 127.0.0.1:18608");
+        assertTrue(querySource.contains("criteriaBuilder.and(nameTokenPredicates.toArray(new Predicate[0]))"),
+                "Catalog search should require all copied endpoint name tokens to match the same field");
         assertTrue(querySource.contains("TYPE_API"));
         assertTrue(querySource.contains("TYPE_ENDPOINT"));
         assertTrue(querySource.contains("Longs.tryParse(search)"));
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListReadModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListReadModelServiceTest.java
index 5b6346fb79..5c55233b0c 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListReadModelServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListReadModelServiceTest.java
@@ -91,6 +91,10 @@ class EntityListReadModelServiceTest {
                 .build();
         when(entitySummaryReadModelService.loadLatestDefinitionActivities(List.of(teamAlpha)))
                 .thenReturn(Map.of(31L, definitionActivity));
+        EntitySummaryReadModelService.EntitySummaryCounts summaryCounts =
+                new EntitySummaryReadModelService.EntitySummaryCounts(2, 1, 3);
+        when(entitySummaryReadModelService.loadSummaryCounts(List.of(teamAlpha)))
+                .thenReturn(Map.of(31L, summaryCounts));
         List<Monitor> monitors = List.of(
                 Monitor.builder().id(701L).status(CommonConstants.MONITOR_UP_CODE).build()
         );
@@ -99,13 +103,15 @@ class EntityListReadModelServiceTest {
         );
         EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence =
                 new EntityStatusRefreshService.EntityRuntimeStatusEvidence(monitors, List.of(), statusInfo);
-        when(entityStatusRefreshService.refreshEntityStatusWithEvidence(teamAlpha, "team-a")).thenReturn(runtimeEvidence);
+        when(entityStatusRefreshService.refreshEntityStatusesWithEvidence(List.of(teamAlpha), "team-a"))
+                .thenReturn(Map.of(31L, runtimeEvidence));
         EntitySummaryInfo summaryInfo = new EntitySummaryInfo();
         EntityInfo entityInfo = new EntityInfo();
         entityInfo.setId(31L);
         entityInfo.setWorkspaceId("team-a");
         summaryInfo.setEntity(entityInfo);
-        when(entitySummaryReadModelService.buildEntitySummary(teamAlpha, definitionActivity, statusInfo, monitors))
+        when(entitySummaryReadModelService.buildEntitySummary(teamAlpha, definitionActivity, statusInfo, monitors,
+                summaryCounts))
                 .thenReturn(summaryInfo);
 
         Page<EntitySummaryInfo> page = entityListReadModelService.getEntities(
@@ -120,7 +126,8 @@ class EntityListReadModelServiceTest {
                 eq(null), eq("service"), eq(null), eq("checkout"), eq(null), eq(null), eq(null), eq(null), eq(null),
                 eq(null), eq("gmtUpdate"), eq("desc"), eq(0), eq(8), eq("team-a"));
         verify(entitySummaryReadModelService).loadLatestDefinitionActivities(List.of(teamAlpha));
-        verify(entityStatusRefreshService).refreshEntityStatusWithEvidence(teamAlpha, "team-a");
+        verify(entitySummaryReadModelService).loadSummaryCounts(List.of(teamAlpha));
+        verify(entityStatusRefreshService).refreshEntityStatusesWithEvidence(List.of(teamAlpha), "team-a");
     }
 
     @Test
@@ -137,15 +144,19 @@ class EntityListReadModelServiceTest {
                 any(), any(), anyInt(), anyInt(), any()))
                 .thenReturn(new PageImpl<>(List.of(entity), PageRequest.of(0, 10), 42));
         when(entitySummaryReadModelService.loadLatestDefinitionActivities(List.of(entity))).thenReturn(Map.of());
+        EntitySummaryReadModelService.EntitySummaryCounts summaryCounts =
+                new EntitySummaryReadModelService.EntitySummaryCounts(1, 0, 0);
+        when(entitySummaryReadModelService.loadSummaryCounts(List.of(entity))).thenReturn(Map.of(41L, summaryCounts));
         EntityStatusInfo statusInfo = new EntityStatusInfo(
                 "unknown", "no live evidence bound yet", 0, 0, 0, 0, 0, LocalDateTime.now()
         );
         EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence =
                 new EntityStatusRefreshService.EntityRuntimeStatusEvidence(List.of(), List.of(), statusInfo);
-        when(entityStatusRefreshService.refreshEntityStatusWithEvidence(entity, "team-a")).thenReturn(runtimeEvidence);
+        when(entityStatusRefreshService.refreshEntityStatusesWithEvidence(List.of(entity), "team-a"))
+                .thenReturn(Map.of(41L, runtimeEvidence));
         EntitySummaryInfo summaryInfo = new EntitySummaryInfo();
         summaryInfo.setEntity(EntityInfo.fromEntity(entity));
-        when(entitySummaryReadModelService.buildEntitySummary(entity, null, statusInfo, List.of()))
+        when(entitySummaryReadModelService.buildEntitySummary(entity, null, statusInfo, List.of(), summaryCounts))
                 .thenReturn(summaryInfo);
 
         Page<EntitySummaryInfo> page = entityListReadModelService.getEntities(
@@ -155,4 +166,43 @@ class EntityListReadModelServiceTest {
         assertEquals(42, page.getTotalElements());
         assertEquals(41L, page.getContent().getFirst().getEntity().getId());
     }
+
+    @Test
+    void getEntitiesCapsOversizedPagesBeforeRuntimeEvidenceFanout() {
+        ObserveEntity entity = ObserveEntity.builder()
+                .id(51L)
+                .type("service")
+                .name("scale-proof")
+                .status("unknown")
+                .build();
+        when(entityCatalogQueryService.findEntityPage(
+                any(), any(), any(), any(), any(), any(), any(), any(), any(), any(),
+                any(), any(), anyInt(), anyInt()))
+                .thenReturn(new PageImpl<>(List.of(entity), PageRequest.of(0, 50), 1993));
+        when(entitySummaryReadModelService.loadLatestDefinitionActivities(List.of(entity))).thenReturn(Map.of());
+        EntitySummaryReadModelService.EntitySummaryCounts summaryCounts =
+                new EntitySummaryReadModelService.EntitySummaryCounts(1, 0, 0);
+        when(entitySummaryReadModelService.loadSummaryCounts(List.of(entity))).thenReturn(Map.of(51L, summaryCounts));
+        EntityStatusInfo statusInfo = new EntityStatusInfo(
+                "unknown", "no live evidence bound yet", 0, 0, 0, 0, 0, LocalDateTime.now()
+        );
+        EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence =
+                new EntityStatusRefreshService.EntityRuntimeStatusEvidence(List.of(), List.of(), statusInfo);
+        when(entityStatusRefreshService.refreshEntityStatusesWithEvidence(List.of(entity)))
+                .thenReturn(Map.of(51L, runtimeEvidence));
+        EntitySummaryInfo summaryInfo = new EntitySummaryInfo();
+        summaryInfo.setEntity(EntityInfo.fromEntity(entity));
+        when(entitySummaryReadModelService.buildEntitySummary(entity, null, statusInfo, List.of(), summaryCounts))
+                .thenReturn(summaryInfo);
+
+        Page<EntitySummaryInfo> page = entityListReadModelService.getEntities(
+                null, null, null, null, null, null, null, null, null, null,
+                "gmtUpdate", "desc", -4, 1000);
+
+        assertEquals(1993, page.getTotalElements());
+        assertEquals(51L, page.getContent().getFirst().getEntity().getId());
+        verify(entityCatalogQueryService).findEntityPage(
+                eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null),
+                eq("gmtUpdate"), eq("desc"), eq(0), eq(50));
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListRuntimeEvidenceSourceOwnershipTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListRuntimeEvidenceSourceOwnershipTest.java
index fb0ac33dc7..06268bc645 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListRuntimeEvidenceSourceOwnershipTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityListRuntimeEvidenceSourceOwnershipTest.java
@@ -51,11 +51,12 @@ class EntityListRuntimeEvidenceSourceOwnershipTest {
 
         assertTrue(listSource.contains("private final EntityStatusRefreshService entityStatusRefreshService"));
         assertTrue(listSource.contains("EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence"));
-        assertTrue(listSource.contains("entityStatusRefreshService.refreshEntityStatusWithEvidence(entity, requestWorkspaceId)"));
+        assertTrue(listSource.contains("entityStatusRefreshService.refreshEntityStatusesWithEvidence(scopedEntities, requestWorkspaceId)"));
         assertTrue(listSource.contains("runtimeEvidence.monitors()"));
         assertTrue(listSource.contains("runtimeEvidence.statusInfo()"));
 
         assertTrue(statusRefreshSource.contains("public EntityRuntimeStatusEvidence refreshEntityStatusWithEvidence("));
+        assertTrue(statusRefreshSource.contains("public Map<Long, EntityRuntimeStatusEvidence> refreshEntityStatusesWithEvidence("));
         assertTrue(statusRefreshSource.contains("public record EntityRuntimeStatusEvidence("));
         assertTrue(statusRefreshSource.contains("List<Monitor> monitors"));
         assertTrue(statusRefreshSource.contains("List<SingleAlert> activeAlerts"));
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindQueryServiceTest.java
index 66d8d0ac8a..98b8cdf724 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindQueryServiceTest.java
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 import java.util.List;
+import java.util.Map;
 import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
 import org.apache.hertzbeat.manager.dao.EntityMonitorBindDao;
 import org.junit.jupiter.api.Test;
@@ -57,6 +58,30 @@ class EntityMonitorBindQueryServiceTest {
         assertEquals(List.of(monitorBind), binds);
     }
 
+    @Test
+    void findMonitorBindsByEntityIdsBatchesCurrentPageRows() {
+        List<Long> entityIds = List.of(301L, 302L);
+        EntityMonitorBind firstBind = EntityMonitorBind.builder()
+                .id(401L)
+                .entityId(301L)
+                .monitorId(501L)
+                .build();
+        EntityMonitorBind secondBind = EntityMonitorBind.builder()
+                .id(402L)
+                .entityId(302L)
+                .monitorId(502L)
+                .build();
+        when(entityMonitorBindDao.findAllByEntityIdInOrderByEntityIdAscIdAsc(entityIds))
+                .thenReturn(List.of(firstBind, secondBind));
+
+        Map<Long, List<EntityMonitorBind>> bindsByEntityId =
+                entityMonitorBindQueryService.findMonitorBindsByEntityIds(entityIds);
+
+        assertEquals(List.of(firstBind), bindsByEntityId.get(301L));
+        assertEquals(List.of(secondBind), bindsByEntityId.get(302L));
+        verify(entityMonitorBindDao).findAllByEntityIdInOrderByEntityIdAscIdAsc(entityIds);
+    }
+
     @Test
     void findMonitorBindsByMonitorIdReturnsPersistedBinds() {
         EntityMonitorBind monitorBind = EntityMonitorBind.builder()
@@ -84,4 +109,14 @@ class EntityMonitorBindQueryServiceTest {
         assertEquals(2L, entityMonitorBindQueryService.countMonitorBinds(301L));
         verify(entityMonitorBindDao).countByEntityId(301L);
     }
+
+    @Test
+    void countMonitorBindsByEntityIdsBatchesCurrentPageRows() {
+        List<Long> entityIds = List.of(301L, 302L);
+        when(entityMonitorBindDao.countByEntityIdInGroupByEntityId(entityIds))
+                .thenReturn(List.<Object[]>of(new Object[] {301L, 2L}, new Object[] {302L, 1L}));
+
+        assertEquals(Map.of(301L, 2L, 302L, 1L), entityMonitorBindQueryService.countMonitorBindsByEntityIds(entityIds));
+        verify(entityMonitorBindDao).countByEntityIdInGroupByEntityId(entityIds);
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindServiceTest.java
index 9f23ec3cab..9591ab6b46 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindServiceTest.java
@@ -18,6 +18,8 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -106,6 +108,7 @@ class EntityMonitorBindServiceTest {
         EntityMonitorBind missingMonitorId = EntityMonitorBind.builder().build();
         when(entityMonitorQueryService.monitorExists(501L)).thenReturn(true);
         when(entityMonitorQueryService.monitorExists(502L)).thenReturn(false);
+        when(entityMonitorBindQueryService.findMonitorBindsByMonitorId(501L)).thenReturn(List.of());
 
         monitorBindService.replaceMonitorBinds(301L, List.of(validBind, missingMonitorBind, missingMonitorId));
 
@@ -123,6 +126,25 @@ class EntityMonitorBindServiceTest {
         assertEquals(Map.of("service.name", List.of("checkout-api")), saved.getMatchContext());
     }
 
+    @Test
+    void replaceMonitorBindsRejectsMonitorAlreadyBoundToAnotherEntity() {
+        EntityMonitorBind input = EntityMonitorBind.builder()
+                .monitorId(501L)
+                .build();
+        EntityMonitorBind existing = EntityMonitorBind.builder()
+                .entityId(302L)
+                .monitorId(501L)
+                .build();
+        when(entityMonitorQueryService.monitorExists(501L)).thenReturn(true);
+        when(entityMonitorBindQueryService.findMonitorBindsByMonitorId(501L)).thenReturn(List.of(existing));
+
+        IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+                () -> monitorBindService.replaceMonitorBinds(301L, List.of(input)));
+
+        assertEquals("Monitor already bound to another entity: 501.", error.getMessage());
+        verify(entityMonitorBindWriteModelService, never()).replaceMonitorBinds(eq(301L), org.mockito.ArgumentMatchers.anyList());
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void replaceAutoMonitorBindsRefreshesAutoRowsAndKeepsManualBindsProtected() {
@@ -189,6 +211,33 @@ class EntityMonitorBindServiceTest {
         assertEquals(List.of(702L, 701L), monitors.stream().map(Monitor::getId).toList());
     }
 
+    @Test
+    void findEntityMonitorsByEntityIdsBatchesBindAndMonitorLookupsForCurrentPage() {
+        EntityMonitorBind firstBind = EntityMonitorBind.builder()
+                .entityId(301L)
+                .monitorId(702L)
+                .build();
+        EntityMonitorBind secondBind = EntityMonitorBind.builder()
+                .entityId(302L)
+                .monitorId(701L)
+                .build();
+        when(entityMonitorBindQueryService.findMonitorBindsByEntityIds(List.of(301L, 302L, 303L)))
+                .thenReturn(Map.of(301L, List.of(firstBind), 302L, List.of(secondBind)));
+        Monitor firstMonitor = Monitor.builder().id(701L).name("first-returned").build();
+        Monitor secondMonitor = Monitor.builder().id(702L).name("second-returned").build();
+        when(entityMonitorQueryService.findMonitorsByIds(java.util.Set.of(701L, 702L)))
+                .thenReturn(List.of(firstMonitor, secondMonitor));
+
+        Map<Long, List<Monitor>> monitorsByEntityId =
+                monitorBindService.findEntityMonitorsByEntityIds(List.of(301L, 302L, 303L));
+
+        assertEquals(List.of(702L), monitorsByEntityId.get(301L).stream().map(Monitor::getId).toList());
+        assertEquals(List.of(701L), monitorsByEntityId.get(302L).stream().map(Monitor::getId).toList());
+        assertNull(monitorsByEntityId.get(303L));
+        verify(entityMonitorBindQueryService).findMonitorBindsByEntityIds(List.of(301L, 302L, 303L));
+        verify(entityMonitorQueryService).findMonitorsByIds(java.util.Set.of(701L, 702L));
+    }
+
     @Test
     void findEntityMonitorsReturnsEmptyWithoutMonitorDaoLookupWhenNoBindsExist() {
         when(entityMonitorBindQueryService.findMonitorBinds(302L)).thenReturn(List.of());
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMutationWorkflowServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMutationWorkflowServiceTest.java
index 53ab31691e..315d1a520b 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMutationWorkflowServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityMutationWorkflowServiceTest.java
@@ -18,12 +18,20 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.inOrder;
+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 java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
 import org.apache.hertzbeat.common.entity.manager.EntityRelation;
@@ -36,6 +44,8 @@ import org.mockito.InOrder;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 /**
  * Contract for entity mutation workflow ordering across create, edit, and definition import paths.
@@ -195,6 +205,132 @@ class EntityMutationWorkflowServiceTest {
                 .recordDefinitionActivity(102L, "definition_import", "yaml", secondSaved);
     }
 
+    @Test
+    void addEntityByDefinitionRejectsExistingEntityReferenceBeforeWrite() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        EntityDto entityDto = new EntityDto();
+        ObserveEntity input = ObserveEntity.builder()
+                .type("service")
+                .namespace("product-design")
+                .name("checkout")
+                .build();
+        entityDto.setEntity(input);
+        when(entityDefinitionDraftService.parseEntityDefinition(request, null)).thenReturn(entityDto);
+        when(entityWorkspaceAccessService.findAccessibleEntityByReferenceForRequestWorkspace(
+                "service", "product-design", "checkout"))
+                .thenReturn(Optional.of(ObserveEntity.builder().id(44L).build()));
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> entityMutationWorkflowService.addEntityByDefinition(request));
+
+        assertEquals("Entity already exists: service product-design/checkout.", thrown.getMessage());
+        verify(entityCoreWriteModelService, never()).createEntity(input, "manual");
+    }
+
+    @Test
+    void addEntityRejectsExistingEntityReferenceBeforeWrite() {
+        EntityDto entityDto = new EntityDto();
+        ObserveEntity input = ObserveEntity.builder()
+                .type("service")
+                .namespace("product-design")
+                .name("checkout")
+                .build();
+        entityDto.setEntity(input);
+        when(entityWorkspaceAccessService.findAccessibleEntityByNameForRequestWorkspace("checkout"))
+                .thenReturn(Optional.of(ObserveEntity.builder()
+                        .id(44L)
+                        .type("endpoint")
+                        .name("checkout")
+                        .build()));
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> entityMutationWorkflowService.addEntity(entityDto));
+
+        assertEquals("Entity already exists: checkout.", thrown.getMessage());
+        verify(entityCoreWriteModelService, never()).createEntity(input, "manual");
+    }
+
+    @Test
+    void addEntitiesByDefinitionBundleRejectsDuplicateSubmittedReferenceBeforeWrite() {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        EntityDto firstDto = new EntityDto();
+        ObserveEntity firstInput = ObserveEntity.builder()
+                .type("service")
+                .namespace("product-design")
+                .name("checkout")
+                .build();
+        firstDto.setEntity(firstInput);
+        EntityDto secondDto = new EntityDto();
+        ObserveEntity secondInput = ObserveEntity.builder()
+                .type("service")
+                .namespace("product-design")
+                .name("checkout")
+                .build();
+        secondDto.setEntity(secondInput);
+        when(entityDefinitionDraftService.parseEntityDefinitionBundle(request))
+                .thenReturn(List.of(firstDto, secondDto));
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> entityMutationWorkflowService.addEntitiesByDefinitionBundle(request));
+
+        assertEquals("Entity already exists in definition bundle: checkout.", thrown.getMessage());
+        verify(entityCoreWriteModelService, never()).createEntities(List.of(firstInput, secondInput), "manual");
+    }
+
+    @Test
+    void addEntitiesByDefinitionBundleHoldsSameReferenceLockUntilTransactionCompletion() throws Exception {
+        EntityDefinitionRequest request = new EntityDefinitionRequest();
+        request.setFormat("yaml");
+        EntityDto firstDto = new EntityDto();
+        ObserveEntity firstInput = ObserveEntity.builder()
+                .type("service")
+                .namespace("product-design")
+                .name("checkout")
+                .build();
+        firstDto.setEntity(firstInput);
+        EntityDto secondDto = new EntityDto();
+        ObserveEntity secondInput = ObserveEntity.builder()
+                .type("service")
+                .namespace("product-design")
+                .name("payments")
+                .build();
+        secondDto.setEntity(secondInput);
+        ObserveEntity firstSaved = ObserveEntity.builder().id(101L).name("checkout").build();
+        ObserveEntity secondSaved = ObserveEntity.builder().id(102L).name("payments").build();
+        when(entityDefinitionDraftService.parseEntityDefinitionBundle(request))
+                .thenReturn(List.of(firstDto, secondDto));
+        when(entityWorkspaceAccessService.findAccessibleEntityByReferenceForRequestWorkspace(
+                "service", "product-design", "checkout"))
+                .thenReturn(Optional.empty());
+        when(entityWorkspaceAccessService.findAccessibleEntityByReferenceForRequestWorkspace(
+                "service", "product-design", "payments"))
+                .thenReturn(Optional.empty());
+        when(entityCoreWriteModelService.createEntities(List.of(firstInput, secondInput), "manual"))
+                .thenReturn(List.of(firstSaved, secondSaved));
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+
+        TransactionSynchronizationManager.initSynchronization();
+        try {
+            assertEquals(List.of(101L, 102L), entityMutationWorkflowService.addEntitiesByDefinitionBundle(request));
+            Future<List<Long>> blockedImport = executor.submit(
+                    () -> entityMutationWorkflowService.addEntitiesByDefinitionBundle(request));
+
+            TimeUnit.MILLISECONDS.sleep(150);
+            assertFalse(blockedImport.isDone());
+
+            TransactionSynchronizationManager.getSynchronizations()
+                    .forEach(synchronization -> synchronization.afterCompletion(TransactionSynchronization.STATUS_COMMITTED));
+            TransactionSynchronizationManager.clearSynchronization();
+
+            assertEquals(List.of(101L, 102L), blockedImport.get(2, TimeUnit.SECONDS));
+        } finally {
+            if (TransactionSynchronizationManager.isSynchronizationActive()) {
+                TransactionSynchronizationManager.clearSynchronization();
+            }
+            executor.shutdownNow();
+        }
+    }
+
     @Test
     void modifyEntityByDefinitionRecordsFailureActivityBeforeRethrow() {
         EntityDefinitionRequest request = new EntityDefinitionRequest();
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQueryServiceTest.java
index bddc6c7fe0..2755ce8d2c 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQueryServiceTest.java
@@ -18,6 +18,8 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.List;
@@ -82,7 +84,7 @@ class EntityNoiseControlRuleQueryServiceTest {
                         CommonConstants.LABEL_INSTANCE, "checkout-api.default.svc.cluster.local"
                 ))
                 .build();
-        when(alertSilenceDao.findAll()).thenReturn(List.of(disabled, globalMatchAll, teamBeta, teamAlpha));
+        when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(List.of(globalMatchAll, teamBeta, teamAlpha));
 
         List<AlertSilence> scopedRules = entityNoiseControlRuleQueryService.findEnabledSilences("team-a");
         List<AlertSilence> defaultRules = entityNoiseControlRuleQueryService.findEnabledSilences(
@@ -90,6 +92,7 @@ class EntityNoiseControlRuleQueryServiceTest {
 
         assertEquals(List.of(teamAlpha), scopedRules);
         assertEquals(List.of(globalMatchAll), defaultRules);
+        verify(alertSilenceDao, never()).findAll();
     }
 
     @Test
@@ -122,7 +125,7 @@ class EntityNoiseControlRuleQueryServiceTest {
                         CommonConstants.LABEL_INSTANCE, "checkout-api.default.svc.cluster.local"
                 ))
                 .build();
-        when(alertInhibitDao.findAll()).thenReturn(List.of(disabled, generic, teamBeta, teamAlpha));
+        when(alertInhibitDao.findAlertInhibitsByEnableIsTrue()).thenReturn(List.of(generic, teamBeta, teamAlpha));
 
         List<AlertInhibit> scopedRules = entityNoiseControlRuleQueryService.findEnabledInhibits("team-a");
         List<AlertInhibit> defaultRules = entityNoiseControlRuleQueryService.findEnabledInhibits(
@@ -130,5 +133,6 @@ class EntityNoiseControlRuleQueryServiceTest {
 
         assertEquals(List.of(teamAlpha), scopedRules);
         assertEquals(List.of(generic), defaultRules);
+        verify(alertInhibitDao, never()).findAll();
     }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQuerySourceOwnershipTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQuerySourceOwnershipTest.java
index c83f26e5c3..63fc3dfe88 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQuerySourceOwnershipTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQuerySourceOwnershipTest.java
@@ -61,6 +61,12 @@ class EntityNoiseControlRuleQuerySourceOwnershipTest {
         assertTrue(ruleQuerySource.contains("import org.apache.hertzbeat.alert.dao.AlertInhibitDao"));
         assertTrue(ruleQuerySource.contains("public List<AlertSilence> findEnabledSilences(String requestWorkspaceId)"));
         assertTrue(ruleQuerySource.contains("public List<AlertInhibit> findEnabledInhibits(String requestWorkspaceId)"));
+        assertFalse(ruleQuerySource.contains("alertSilenceDao.findAll()"),
+                "Entity detail should not scan every silence rule before workspace and enable filtering");
+        assertFalse(ruleQuerySource.contains("alertInhibitDao.findAll()"),
+                "Entity detail should not scan every inhibit rule before workspace and enable filtering");
+        assertTrue(ruleQuerySource.contains("alertSilenceDao.findAlertSilencesByEnableTrue()"));
+        assertTrue(ruleQuerySource.contains("alertInhibitDao.findAlertInhibitsByEnableIsTrue()"));
         assertTrue(ruleQuerySource.contains("matchesNoiseControlRuleRequestWorkspace("));
         assertTrue(ruleQuerySource.contains("AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(normalizedRequestWorkspaceId)"));
     }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationQueryServiceTest.java
index 99eb7b6e2a..1ba2ab020d 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationQueryServiceTest.java
@@ -18,12 +18,16 @@
 package org.apache.hertzbeat.manager.service.entity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.List;
+import java.util.Map;
 import org.apache.hertzbeat.common.entity.manager.EntityRelation;
 import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.springframework.data.domain.PageRequest;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
@@ -57,6 +61,28 @@ class EntityRelationQueryServiceTest {
         assertEquals(List.of(relation), relations);
     }
 
+    @Test
+    void findEntityRelationsWithLimitReturnsStablePreviewRows() {
+        EntityRelation relation = EntityRelation.builder()
+                .id(31L)
+                .sourceEntityId(10L)
+                .targetEntityId(30L)
+                .relationType("calls")
+                .build();
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(eq(10L), eq(10L), any(PageRequest.class)))
+                .thenReturn(List.of(relation));
+
+        List<EntityRelation> relations = entityRelationQueryService.findEntityRelations(10L, 50);
+
+        assertEquals(List.of(relation), relations);
+        verify(entityRelationDao).findBySourceEntityIdOrTargetEntityId(eq(10L), eq(10L), any(PageRequest.class));
+    }
+
+    @Test
+    void findEntityRelationsWithNonPositiveLimitSkipsStorage() {
+        assertEquals(List.of(), entityRelationQueryService.findEntityRelations(10L, 0));
+    }
+
     @Test
     void countEntityRelationsReturnsIncomingAndOutgoingCount() {
         when(entityRelationDao.countBySourceEntityIdOrTargetEntityId(10L, 10L)).thenReturn(3L);
@@ -64,4 +90,17 @@ class EntityRelationQueryServiceTest {
         assertEquals(3L, entityRelationQueryService.countEntityRelations(10L));
         verify(entityRelationDao).countBySourceEntityIdOrTargetEntityId(10L, 10L);
     }
+
+    @Test
+    void countEntityRelationsByEntityIdsMergesIncomingAndOutgoingCounts() {
+        List<Long> entityIds = List.of(10L, 20L);
+        when(entityRelationDao.countBySourceEntityIdInGroupBySourceEntityId(entityIds))
+                .thenReturn(List.<Object[]>of(new Object[] {10L, 2L}));
+        when(entityRelationDao.countByTargetEntityIdInGroupByTargetEntityId(entityIds))
+                .thenReturn(List.<Object[]>of(new Object[] {10L, 1L}, new Object[] {20L, 4L}));
+
+        assertEquals(Map.of(10L, 3L, 20L, 4L), entityRelationQueryService.countEntityRelationsByEntityIds(entityIds));
+        verify(entityRelationDao).countBySourceEntityIdInGroupBySourceEntityId(entityIds);
+        verify(entityRelationDao).countByTargetEntityIdInGroupByTargetEntityId(entityIds);
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationServiceTest.java
index 1ca437e492..cc5279dff9 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityRelationServiceTest.java
@@ -76,6 +76,21 @@ class EntityRelationServiceTest {
         assertEquals(List.of(relation), relations);
     }
 
+    @Test
+    void findEntityRelationsWithLimitDelegatesToPreviewQuery() {
+        EntityRelation relation = EntityRelation.builder()
+                .id(31L)
+                .sourceEntityId(10L)
+                .targetEntityId(30L)
+                .relationType("calls")
+                .build();
+        when(entityRelationQueryService.findEntityRelations(10L, 50)).thenReturn(List.of(relation));
+
+        List<EntityRelation> relations = entityRelationService.findEntityRelations(10L, 50);
+
+        assertEquals(List.of(relation), relations);
+    }
+
     @Test
     void countEntityRelationsReturnsIncomingAndOutgoingCount() {
         when(entityRelationQueryService.countEntityRelations(10L)).thenReturn(3L);
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshServiceTest.java
index f526f97962..34fc702113 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshServiceTest.java
@@ -17,11 +17,15 @@
 
 package org.apache.hertzbeat.manager.service.entity;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.mockito.Mockito.inOrder;
+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.Map;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
@@ -113,4 +117,46 @@ class EntityStatusRefreshServiceTest {
         inOrder.verify(entityAlertEvidenceReadModelService).queryActiveAlerts(monitors, 20);
         inOrder.verify(entityRuntimeHealthService).refreshEntityStatus(entity, monitors, activeAlerts);
     }
+
+    @Test
+    void refreshEntityStatusesWithEvidenceBatchesCurrentPageMonitorLookup() {
+        ObserveEntity checkout = ObserveEntity.builder()
+                .id(42L)
+                .type("service")
+                .name("checkout-api")
+                .workspaceId("team-a")
+                .build();
+        ObserveEntity payments = ObserveEntity.builder()
+                .id(43L)
+                .type("service")
+                .name("payments-api")
+                .workspaceId("team-a")
+                .build();
+        Monitor monitor = Monitor.builder().id(101L).name("checkout-instance").build();
+        List<SingleAlert> activeAlerts = List.of(SingleAlert.builder().id(201L).status("firing").build());
+        EntityStatusInfo checkoutStatus = new EntityStatusInfo(
+                "critical", "1 firing alerts", 1, 0, 1, 0, 1, null);
+        EntityStatusInfo paymentsStatus = new EntityStatusInfo(
+                "unknown", "no live evidence bound yet", 0, 0, 0, 0, 0, null);
+        when(entityMonitorBindService.findEntityMonitorsByEntityIds(List.of(42L, 43L)))
+                .thenReturn(Map.of(42L, List.of(monitor)));
+        when(entityAlertEvidenceReadModelService.queryActiveAlerts(List.of(monitor), 20, "team-a"))
+                .thenReturn(activeAlerts);
+        when(entityRuntimeHealthService.refreshEntityStatus(checkout, List.of(monitor), activeAlerts))
+                .thenReturn(checkoutStatus);
+        when(entityRuntimeHealthService.refreshEntityStatus(payments, List.of(), List.of()))
+                .thenReturn(paymentsStatus);
+
+        Map<Long, EntityStatusRefreshService.EntityRuntimeStatusEvidence> evidenceByEntity =
+                statusRefreshService.refreshEntityStatusesWithEvidence(List.of(checkout, payments), "team-a");
+
+        assertSame(checkoutStatus, evidenceByEntity.get(42L).statusInfo());
+        assertSame(paymentsStatus, evidenceByEntity.get(43L).statusInfo());
+        assertEquals(List.of(monitor), evidenceByEntity.get(42L).monitors());
+        assertEquals(List.of(), evidenceByEntity.get(43L).monitors());
+        verify(entityMonitorBindService).findEntityMonitorsByEntityIds(List.of(42L, 43L));
+        verify(entityMonitorBindService, never()).findEntityMonitors(42L);
+        verify(entityMonitorBindService, never()).findEntityMonitors(43L);
+        verify(entityAlertEvidenceReadModelService, never()).queryActiveAlerts(List.of(), 20, "team-a");
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshSourceOwnershipTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshSourceOwnershipTest.java
index d152e36c99..8aea9d8253 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshSourceOwnershipTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityStatusRefreshSourceOwnershipTest.java
@@ -58,8 +58,7 @@ class EntityStatusRefreshSourceOwnershipTest {
         assertFalse(statusRefreshSource.contains("currentRequestWorkspaceId()"),
                 "Status refresh should delegate default active-alert workspace lookup to the alert query boundary");
         assertTrue(statusRefreshSource.contains("return refreshEntityStatusWithEvidence(entity).statusInfo()"));
-        assertTrue(statusRefreshSource.contains("entityAlertEvidenceReadModelService.queryActiveAlerts(\n"
-                + "                monitors, ACTIVE_ALERT_LIMIT)"));
+        assertTrue(statusRefreshSource.contains("entityAlertEvidenceReadModelService.queryActiveAlerts(monitors, ACTIVE_ALERT_LIMIT)"));
         assertTrue(alertEvidenceSource.contains("public List<SingleAlert> queryActiveAlerts(List<Monitor> monitors, "
                 + "int limit)"));
         assertTrue(alertQuerySource.contains("public List<SingleAlert> findActiveAlerts(List<Monitor> monitors, "
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelServiceTest.java
index ecb81dc562..5c1ef3a5e5 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntitySummaryReadModelServiceTest.java
@@ -164,4 +164,63 @@ class EntitySummaryReadModelServiceTest {
         assertEquals(Map.of(), latestActivities);
         verify(entityActivityReadModelService, never()).findLatestDefinitionActivities(any());
     }
+
+    @Test
+    void loadSummaryCountsBatchesCurrentPageCountEvidence() {
+        ObserveEntity firstEntity = ObserveEntity.builder().id(501L).name("checkout-api").build();
+        ObserveEntity secondEntity = ObserveEntity.builder().id(502L).name("billing-api").build();
+        List<Long> entityIds = List.of(501L, 502L);
+        when(entityIdentityReadModelService.countIdentitiesByEntityIds(entityIds))
+                .thenReturn(Map.of(501L, 2L, 502L, 1L));
+        when(entityMonitorBindService.countMonitorBindsByEntityIds(entityIds))
+                .thenReturn(Map.of(501L, 3L));
+        when(entityRelationService.countEntityRelationsByEntityIds(entityIds))
+                .thenReturn(Map.of(502L, 4L));
+
+        Map<Long, EntitySummaryReadModelService.EntitySummaryCounts> counts =
+                summaryReadModelService.loadSummaryCounts(List.of(firstEntity, secondEntity));
+
+        assertEquals(2, counts.size());
+        assertEquals(new EntitySummaryReadModelService.EntitySummaryCounts(2, 3, 0), counts.get(501L));
+        assertEquals(new EntitySummaryReadModelService.EntitySummaryCounts(1, 0, 4), counts.get(502L));
+        verify(entityIdentityReadModelService).countIdentitiesByEntityIds(entityIds);
+        verify(entityMonitorBindService).countMonitorBindsByEntityIds(entityIds);
+        verify(entityRelationService).countEntityRelationsByEntityIds(entityIds);
+    }
+
+    @Test
+    void buildEntitySummaryAcceptsPreloadedCountsForListPages() {
+        ObserveEntity entity = ObserveEntity.builder()
+                .id(601L)
+                .type("service")
+                .name("checkout-api")
+                .status("unknown")
+                .build();
+        EntityStatusInfo statusInfo = new EntityStatusInfo(
+                "unknown", "no live evidence", 0, 0, 0, 0, 0, LocalDateTime.now()
+        );
+        EntityEvidenceSummaryInfo evidenceSummary = new EntityEvidenceSummaryInfo(0, 0, 0, 5, 0, 1778340000000L);
+        when(entityObservabilityGateway.buildEntityEvidenceSummary(
+                eq(entity), eq(statusInfo), eq(5L), eq(0), eq(Collections.emptyList()), eq(Collections.emptyList())
+        )).thenReturn(evidenceSummary);
+        EntityOpsSummaryInfo opsSummary = new EntityOpsSummaryInfo(true, false, true, true, true, 80);
+        when(entityObservabilityGateway.buildEntityOpsSummary(entity, 7L, evidenceSummary)).thenReturn(opsSummary);
+        when(entityObservabilityGateway.buildEntityNextActions(entity, evidenceSummary, null, opsSummary))
+                .thenReturn(List.of());
+
+        EntitySummaryInfo summary = summaryReadModelService.buildEntitySummary(
+                entity,
+                null,
+                statusInfo,
+                List.of(),
+                new EntitySummaryReadModelService.EntitySummaryCounts(5, 6, 7)
+        );
+
+        assertEquals(5, summary.getIdentityCount());
+        assertEquals(6, summary.getMonitorCount());
+        assertEquals(7, summary.getRelationCount());
+        verify(entityIdentityReadModelService, never()).countIdentities(601L);
+        verify(entityMonitorBindService, never()).countMonitorBinds(601L);
+        verify(entityRelationService, never()).countEntityRelations(601L);
+    }
 }
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 1161ee54b7..c48596c194 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
@@ -300,6 +300,25 @@ class EntityWorkspaceAccessServiceTest {
         assertSame(teamAlphaEntity, accessibleEntity.get());
     }
 
+    @Test
+    void findAccessibleEntityByNameForRequestWorkspaceUsesCurrentRequestScope() {
+        ObserveEntity teamAlphaEntity = ObserveEntity.builder()
+                .id(433L)
+                .type("endpoint")
+                .name("checkout")
+                .workspaceId("team-a")
+                .build();
+        when(entityWorkspaceQueryService.findEntityByName("team-a", "checkout"))
+                .thenReturn(Optional.of(teamAlphaEntity));
+        AuthTokenRequestContext.bindWorkspaceId(" team-a ");
+
+        Optional<ObserveEntity> accessibleEntity =
+                workspaceAccessService.findAccessibleEntityByNameForRequestWorkspace("checkout");
+
+        assertTrue(accessibleEntity.isPresent());
+        assertSame(teamAlphaEntity, accessibleEntity.get());
+    }
+
     @Test
     void resolveWriteWorkspaceIdPrefersRequestThenSourceThenCurrentThenDefault() {
         AuthTokenRequestContext.bindWorkspaceId(" team-a ");
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQuerySourceOwnershipTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQuerySourceOwnershipTest.java
index 280b321837..8840939949 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQuerySourceOwnershipTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQuerySourceOwnershipTest.java
@@ -54,6 +54,7 @@ class EntityWorkspaceQuerySourceOwnershipTest {
         assertTrue(accessSource.contains("entityWorkspaceQueryService.findEntities(normalizedWorkspaceId, sort)"));
         assertTrue(accessSource.contains("entityWorkspaceQueryService.findEntityById(entityId)"));
         assertTrue(accessSource.contains("entityWorkspaceQueryService.findEntityByReference("));
+        assertTrue(accessSource.contains("entityWorkspaceQueryService.findEntityByName(normalizedWorkspaceId, name)"));
         assertTrue(accessSource.contains("normalizedWorkspaceId, type, namespace, name"));
         assertTrue(accessSource.contains("normalizedWorkspaceId, type, name"));
 
@@ -76,5 +77,8 @@ class EntityWorkspaceQuerySourceOwnershipTest {
         assertTrue(querySource.contains(
                 "observeEntityDao.findFirstByWorkspaceIdAndTypeAndName(workspaceId, type, name)"));
         assertTrue(querySource.contains("observeEntityDao.findFirstByTypeAndName(type, name)"));
+        assertTrue(querySource.contains("public Optional<ObserveEntity> findEntityByName(String workspaceId, String name)"));
+        assertTrue(querySource.contains("observeEntityDao.findFirstByWorkspaceIdAndName(workspaceId, name)"));
+        assertTrue(querySource.contains("observeEntityDao.findFirstByName(name)"));
     }
 }
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
index dc38ac840d..5b11cf75d7 100644
--- 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
@@ -142,6 +142,9 @@ class LocalTopologyDemoRelationSeederTest {
         ArgumentCaptor<List<ObserveEntity>> entitiesCaptor = ArgumentCaptor.forClass(List.class);
         verify(observeEntityDao).saveAll(entitiesCaptor.capture());
         assertEquals(1993, entitiesCaptor.getValue().size());
+        ArgumentCaptor<List<EntityRelation>> relationsCaptor = ArgumentCaptor.forClass(List.class);
+        verify(entityRelationDao).saveAll(relationsCaptor.capture());
+        assertEquals(1992, relationsCaptor.getValue().size());
     }
 
     @Test
@@ -156,21 +159,37 @@ class LocalTopologyDemoRelationSeederTest {
 
         ArgumentCaptor<List<ObserveEntity>> entitiesCaptor = ArgumentCaptor.forClass(List.class);
         ArgumentCaptor<List<EntityIdentity>> identitiesCaptor = ArgumentCaptor.forClass(List.class);
+        ArgumentCaptor<List<EntityRelation>> relationsCaptor = ArgumentCaptor.forClass(List.class);
         verify(observeEntityDao).saveAll(entitiesCaptor.capture());
         verify(entityIdentityDao).saveAll(identitiesCaptor.capture());
+        verify(entityRelationDao).saveAll(relationsCaptor.capture());
         List<ObserveEntity> entities = entitiesCaptor.getValue();
         List<EntityIdentity> identities = identitiesCaptor.getValue();
+        List<EntityRelation> relations = relationsCaptor.getValue();
         assertEquals(1993, entities.size());
         assertEquals(1993, identities.size());
+        assertEquals(1992, relations.size());
         assertEquals("hb-mix-1780329856-edge-gateway", entities.getFirst().getName());
         assertEquals("scale-mix", entities.getFirst().getNamespace());
         assertEquals("prod", entities.getFirst().getEnvironment());
+        assertEquals("local-scale-proof", entities.getFirst().getSource());
         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("local-scale-proof", entities.getLast().getSource());
         assertEquals("hb-mix-1780329856-svc-11-164", identities.getLast().getIdentityValue());
+        EntityRelation gatewayToDomain = relations.getFirst();
+        assertEquals(646566130000000L, gatewayToDomain.getSourceEntityId());
+        assertEquals(646566130000001L, gatewayToDomain.getTargetEntityId());
+        assertEquals("trace-call", gatewayToDomain.getRelationType());
+        assertEquals("otlp-trace-call", gatewayToDomain.getRelationSource());
+        assertEquals("local-topology-demo-seed", gatewayToDomain.getAttributes().get("seed"));
+        EntityRelation lastDomainToService = relations.getLast();
+        assertEquals(646566130000012L, lastDomainToService.getSourceEntityId());
+        assertEquals(646566130001992L, lastDomainToService.getTargetEntityId());
+        assertEquals("trace-call", lastDomainToService.getRelationType());
     }
 
     @Test
@@ -207,6 +226,9 @@ class LocalTopologyDemoRelationSeederTest {
         assertEquals("hb-mix-1780329856-edge-gateway", gatewayIdentity.getIdentityValue());
         assertEquals("hb-mix-1780329856-edge-gateway", gatewayIdentity.getNormalizedValue());
         assertEquals(1993, identities.size());
+        ArgumentCaptor<List<EntityRelation>> relationsCaptor = ArgumentCaptor.forClass(List.class);
+        verify(entityRelationDao).saveAll(relationsCaptor.capture());
+        assertEquals(1992, relationsCaptor.getValue().size());
     }
 
     private ObserveEntity entity(Long id, String type, String name) {
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 ca1080546a..b6025abd5d 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
@@ -1169,9 +1169,13 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
             operationNameMatch.remove("http_route");
             LinkedHashMap<String, String> httpRouteMatch = new LinkedHashMap<>(resourceMatch);
             httpRouteMatch.remove("operation_name");
+            LinkedHashMap<String, String> serviceMatch = new LinkedHashMap<>(resourceMatch);
+            serviceMatch.remove("operation_name");
+            serviceMatch.remove("http_route");
             return List.of(
                     buildRelatedMetricAvailabilityProbe(metricName, candidate, operationNameMatch),
-                    buildRelatedMetricAvailabilityProbe(metricName, candidate, httpRouteMatch)
+                    buildRelatedMetricAvailabilityProbe(metricName, candidate, httpRouteMatch),
+                    buildRelatedMetricAvailabilityProbe(metricName, candidate, serviceMatch)
             );
         }
         return List.of(buildRelatedMetricAvailabilityProbe(metricName, candidate, resourceMatch));
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 5ac0bdc5f4..88c3edeb88 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
@@ -250,7 +250,8 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa
         readinessScore += relationReady ? 20 : 0;
         readinessScore += telemetryReady ? 20 : 0;
         readinessScore += statusReady ? 20 : 0;
-        return new EntityOpsSummaryInfo(ownerReady, runbookReady, relationReady, telemetryReady, statusReady, readinessScore);
+        return new EntityOpsSummaryInfo(ownerReady, runbookReady, relationReady, telemetryReady,
+                statusReady, readinessScore, Math.max(0L, relationCount));
     }
 
     @Override
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 c8f8a4e706..727fecb890 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
@@ -666,54 +666,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
         if (results.isEmpty()) {
             results.addAll(buildStoredLogEvidence(entityContext, entityId, entityIdentityKeys, preferredHint, logSummary, bindingResult));
         }
-        if (!results.isEmpty()) {
-            return results;
-        }
-        if (preferredHint == null && !hasLogSummaryQuerySignal(logSummary)) {
-            return Collections.emptyList();
-        }
-        Map<String, String> resource = preferredHint == null || preferredHint.getResourceFilters() == null
-                ? Collections.emptyMap()
-                : preferredHint.getResourceFilters();
-        List<String> searchTerms = preferredHint == null || CollectionUtils.isEmpty(preferredHint.getSearchTerms())
-                ? buildFallbackSearchTerms(null, null,
-                defaultText(logSummary == null ? null : logSummary.getPreferredQueryTitle(),
-                        logSummary == null ? null : logSummary.getFallbackSearchTerm()))
-                : preferredHint.getSearchTerms();
-        TelemetryIdentitySnapshot identitySnapshot =
-                buildEntityIdentitySnapshot(entityContext, SOURCE_OTLP, SIGNAL_LOGS, System.currentTimeMillis());
-        String queryHint = firstNonBlank(
-                preferredHint == null ? null : preferredHint.getTraceId(),
-                preferredHint == null ? null : preferredHint.getTitle(),
-                logSummary == null ? null : logSummary.getPreferredQueryTitle(),
-                logSummary == null ? null : logSummary.getFallbackSearchTerm()
-        );
-        return List.of(new LogEvidence(
-                SOURCE_OTLP,
-                SIGNAL_LOGS,
-                entityId == 0L ? null : entityId,
-                identitySnapshot,
-                System.currentTimeMillis(),
-                "attention",
-                queryHint,
-                bindingResult,
-                buildCodeNavigationHint(entityContext, resource, Collections.emptyMap(), searchTerms, queryHint),
-                defaultText(logSummary == null ? null : logSummary.getPreferredQueryTitle(), queryHint),
-                null,
-                preferredHint == null ? null : preferredHint.getTraceId(),
-                preferredHint == null ? null : preferredHint.getSpanId(),
-                resource,
-                searchTerms
-        ));
-    }
-
-    private boolean hasLogSummaryQuerySignal(EntityLogSummaryInfo logSummary) {
-        return logSummary != null
-                && (logSummary.getHintCount() > 0
-                || StringUtils.hasText(logSummary.getPreferredQueryTitle())
-                || StringUtils.hasText(logSummary.getFallbackSearchTerm())
-                || !CollectionUtils.isEmpty(logSummary.getPreferredResourceFilters())
-                || !CollectionUtils.isEmpty(logSummary.getPreferredSearchTerms()));
+        return results;
     }
 
     private List<LogEvidence> buildStoredLogEvidence(ObservedEntityContext entityContext,
@@ -801,9 +754,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                 continue;
             }
             recentTraceCount++;
-            if (StringUtils.hasText(signal.errorState())
-                    && !"ok".equalsIgnoreCase(signal.errorState())
-                    && !"false".equalsIgnoreCase(signal.errorState())) {
+            if (isTraceErrorState(signal.errorState())) {
                 recentErrorTraceCount++;
             }
             if (latestObservedAt == null || (signal.observedAt() != null && signal.observedAt() > latestObservedAt)) {
@@ -820,6 +771,19 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
         );
     }
 
+    private boolean isTraceErrorState(String errorState) {
+        String normalized = trimToNull(errorState);
+        if (!StringUtils.hasText(normalized)) {
+            return false;
+        }
+        String lower = normalized.toLowerCase(Locale.ROOT);
+        return "error".equals(lower)
+                || "status_code_error".equals(lower)
+                || "true".equals(lower)
+                || "failed".equals(lower)
+                || "failure".equals(lower);
+    }
+
     @Override
     public List<EntityTraceQueryHintDto> buildTraceQueryHints(ObservedEntityContext entityContext) {
         long entityId = entityId(entityContext);
@@ -945,8 +909,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                 logsActive,
                 tracesActive,
                 metricEvidence == null ? 0 : metricEvidence.size(),
-                Math.max(logSummary == null ? 0 : logSummary.getHintCount(),
-                        logEvidence == null ? 0 : logEvidence.size()),
+                logEvidence == null ? 0 : logEvidence.size(),
                 traceSummary == null ? 0 : traceSummary.getRecentTraceCount(),
                 latestObservedAt,
                 activeSignals
@@ -996,7 +959,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                     now
             );
         }
-        if (logSummary != null && logSummary.getHintCount() > 0 || !CollectionUtils.isEmpty(logEvidence)) {
+        if (!CollectionUtils.isEmpty(logEvidence)) {
             return new EntityTriageRecommendation(
                     "rule",
                     FOCUS_LOGS,
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java
index 51e15e4ee5..08d0061df3 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java
@@ -151,14 +151,6 @@ public class TraceQueryController {
     private String mergeEntityContextResourceFilter(Long entityId, String entityType, String resourceFilter) {
         String normalizedResourceFilter = StringUtils.trimWhitespace(resourceFilter);
         String scopedResourceFilter = normalizedResourceFilter;
-        if (entityId != null && entityId > 0 && (StringUtils.hasText(scopedResourceFilter)
-                ? !scopedResourceFilter.contains(OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID)
-                : true)) {
-            String entityIdFilter = OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID + "=\"" + entityId + "\"";
-            scopedResourceFilter = StringUtils.hasText(scopedResourceFilter)
-                    ? scopedResourceFilter + " and " + entityIdFilter
-                    : entityIdFilter;
-        }
         String normalizedEntityType = StringUtils.trimWhitespace(entityType);
         if (!StringUtils.hasText(normalizedEntityType) || !normalizedEntityType.matches("[A-Za-z0-9_.:-]+")) {
             return scopedResourceFilter;
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 b05a42fb9f..7d5f22dea7 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
@@ -127,7 +127,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
 
     @Override
     public EntityTraceSummaryDto buildEntityTraceSummary(ObservedEntityContext entityContext) {
-        Map<String, Set<String>> identityValues = canonicalIdentityValues(entityContext);
+        Map<String, Set<String>> identityValues = traceQueryIdentityValues(entityContext);
         if (identityValues.isEmpty()) {
             return new EntityTraceSummaryDto(0, 0, null, false, null);
         }
@@ -171,7 +171,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
 
     @Override
     public List<EntityTraceQueryHintDto> buildEntityTraceQueryHints(ObservedEntityContext entityContext) {
-        Map<String, Set<String>> identityValues = canonicalIdentityValues(entityContext);
+        Map<String, Set<String>> identityValues = traceQueryIdentityValues(entityContext);
         if (identityValues.isEmpty()) {
             return Collections.emptyList();
         }
@@ -254,7 +254,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
                                                  Long maxDurationMs, int pageIndex, int pageSize,
                                                  Boolean hideInternal, String spanScope, String attributeFilter) {
         ObservedEntityContext entityContext = entityId == null ? null : loadEntityContext(entityId);
-        Map<String, Set<String>> identityValues = canonicalIdentityValues(entityContext);
+        Map<String, Set<String>> identityValues = traceQueryIdentityValues(entityContext);
         TraceQueryScope queryScope = resolveTraceQueryScope(entityContext, identityValues, serviceName, serviceNamespace, environment);
         ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
                 identityValues, parseResourceFilters(resourceFilter));
@@ -399,7 +399,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
                                              String resourceFilter, String operationName, Long minDurationMs, Long maxDurationMs,
                                              Boolean hideInternal, String spanScope, String attributeFilter) {
         ObservedEntityContext entityContext = entityId == null ? null : loadEntityContext(entityId);
-        Map<String, Set<String>> identityValues = canonicalIdentityValues(entityContext);
+        Map<String, Set<String>> identityValues = traceQueryIdentityValues(entityContext);
         TraceQueryScope queryScope = resolveTraceQueryScope(entityContext, identityValues, serviceName, serviceNamespace, environment);
         ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
                 identityValues, parseResourceFilters(resourceFilter));
@@ -541,7 +541,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             return result;
         }
         ObservedEntityContext entityContext = entityId == null ? null : loadEntityContext(entityId);
-        Map<String, Set<String>> identityValues = canonicalIdentityValues(entityContext);
+        Map<String, Set<String>> identityValues = traceQueryIdentityValues(entityContext);
         TraceQueryScope queryScope = resolveTraceQueryScope(entityContext, identityValues, serviceName, serviceNamespace, environment);
         ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
                 identityValues, parseResourceFilters(resourceFilter));
@@ -1674,6 +1674,27 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         return values.isEmpty() ? Collections.emptyMap() : values;
     }
 
+    private Map<String, Set<String>> traceQueryIdentityValues(ObservedEntityContext entityContext) {
+        Map<String, Set<String>> values = canonicalIdentityValues(entityContext);
+        if (values.size() <= 1
+                || !values.containsKey(OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID)
+                || hasExplicitEntityIdIdentity(entityContext)) {
+            return values;
+        }
+        Map<String, Set<String>> relaxedValues = new LinkedHashMap<>(values);
+        relaxedValues.remove(OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID);
+        return relaxedValues.isEmpty() ? values : relaxedValues;
+    }
+
+    private boolean hasExplicitEntityIdIdentity(ObservedEntityContext entityContext) {
+        if (entityContext == null || CollectionUtils.isEmpty(entityContext.getIdentities())) {
+            return false;
+        }
+        return entityContext.getIdentities().stream()
+                .anyMatch(identity -> OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID.equals(
+                        trimText(identity.getIdentityKey())));
+    }
+
     private List<String> preferredSearchTerms(Map<String, Set<String>> identityValues) {
         List<String> preferredKeys = List.of("service.name", "service.instance.id", "host.name", "k8s.deployment.name", "cloud.resource_id");
         List<String> terms = new ArrayList<>();
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 71b1772da3..b77e6ce852 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
@@ -1332,6 +1332,91 @@ class OtlpIngestionWorkspaceServiceImplTest {
         assertFalse(candidate.getResourceMatch().containsKey("operation_name"));
     }
 
+    @Test
+    void relatedMetricsFallsBackToServiceAvailabilityWhenOperationLabelsAreAbsent() {
+        observabilitySignalIntakeGateway.recordOtlpMetricIntake(
+                Map.of(
+                        "service.name", "checkout",
+                        "service.namespace", "commerce",
+                        "deployment.environment.name", "prod"
+                ),
+                2_000L,
+                "http.server.duration",
+                "histogram",
+                "ms",
+                14.0,
+                Map.of()
+        );
+        when(workspaceQueryGateway.findEntityById(42L)).thenReturn(java.util.Optional.empty());
+        when(workspaceQueryGateway.findIdentitiesByEntityId(42L)).thenReturn(List.of());
+        when(metricQueryRepository.hasPromqlExecutor()).thenReturn(true);
+        when(metricQueryRepository.queryPromqlRange(
+                anyString(),
+                anyString(),
+                anyLong(),
+                anyLong(),
+                anyString()
+        )).thenAnswer(invocation -> {
+            String refId = invocation.getArgument(0);
+            String query = invocation.getArgument(1);
+            if ("otlp-related-metrics-inventory".equals(refId)) {
+                return promqlSuccess(new DatasourceQueryData("otlp-related-metrics-inventory", 200, null, List.of()));
+            }
+            if (query.contains("__name__=\"http_server_duration\"")
+                    && query.contains("service_name=\"checkout\"")
+                    && query.contains("service_namespace=\"commerce\"")
+                    && query.contains("deployment_environment_name=\"prod\"")
+                    && !query.contains("operation_name=")
+                    && !query.contains("http_route=")) {
+                return promqlSuccess(new DatasourceQueryData(
+                        "otlp-related-metrics",
+                        200,
+                        null,
+                        List.of(new DatasourceQueryData.SchemaData(
+                                new DatasourceQueryData.MetricSchema(
+                                        List.of(
+                                                new DatasourceQueryData.MetricField("__ts__", "time", null),
+                                                new DatasourceQueryData.MetricField("__value__", "number", null)
+                                        ),
+                                        Map.of("__name__", "http_server_duration"),
+                                        Map.of()
+                                ),
+                                Collections.singletonList(new Object[] {2_000L, 14.0})
+                        ))
+                ));
+            }
+            return promqlSuccess(new DatasourceQueryData("otlp-related-metrics", 200, null, List.of()));
+        });
+
+        OtlpRelatedMetricsDto related = otlpIngestionWorkspaceService.getRelatedMetrics(
+                42L,
+                "service",
+                1_000L,
+                2_000L,
+                "checkout",
+                "commerce",
+                "prod",
+                null,
+                "POST /checkout",
+                "8"
+        );
+
+        assertFalse(related.getCandidates().isEmpty());
+        OtlpRelatedMetricsDto.Candidate candidate = related.getCandidates().getFirst();
+        assertEquals("http_server_duration", candidate.getQuery());
+        assertEquals("promql-series", candidate.getReason());
+        assertTrue(candidate.getMatchedLabels().contains("service_name"));
+        assertTrue(candidate.getMatchedLabels().contains("service_namespace"));
+        assertTrue(candidate.getMatchedLabels().contains("deployment_environment_name"));
+        assertFalse(candidate.getMatchedLabels().contains("operation_name"));
+        assertFalse(candidate.getMatchedLabels().contains("http_route"));
+        assertEquals("checkout", candidate.getResourceMatch().get("service_name"));
+        assertEquals("commerce", candidate.getResourceMatch().get("service_namespace"));
+        assertEquals("prod", candidate.getResourceMatch().get("deployment_environment_name"));
+        assertFalse(candidate.getResourceMatch().containsKey("operation_name"));
+        assertFalse(candidate.getResourceMatch().containsKey("http_route"));
+    }
+
     @Test
     void relatedMetricsPrefersPromqlAvailableCandidatesWhenExecutorExists() {
         observabilitySignalIntakeGateway.recordOtlpMetricIntake(
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 875abc8fe6..1d1263aa9a 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
@@ -405,6 +405,7 @@ class EntityObservabilityGatewayImplTest {
         assertTrue(summary.isTelemetryReady());
         assertTrue(summary.isStatusReady());
         assertEquals(100, summary.getReadinessScore());
+        assertEquals(2L, summary.getRelationCount());
 
         EntityOpsSummaryInfo emptySummary = gateway.buildEntityOpsSummary(
                 ObserveEntity.builder().name("empty").build(),
@@ -417,6 +418,7 @@ class EntityObservabilityGatewayImplTest {
         assertFalse(emptySummary.isTelemetryReady());
         assertFalse(emptySummary.isStatusReady());
         assertEquals(0, emptySummary.getReadinessScore());
+        assertEquals(0L, emptySummary.getRelationCount());
     }
 
     @Test
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 331fee5a02..007adce2de 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
@@ -36,6 +36,7 @@ import org.apache.hertzbeat.common.entity.log.LogEntry;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityEvidenceSummaryInfo;
+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.binding.TelemetryIdentitySnapshot;
 import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
@@ -377,6 +378,57 @@ class TelemetryIntakeServiceImplTest {
 
         assertFalse(summary.isLogsActive());
         assertEquals(0, summary.getActiveSignalCount());
+        assertEquals(0, summary.getLogEvidenceCount());
+    }
+
+    @Test
+    void buildLogEvidenceDoesNotConvertQueryHintsIntoFakeEvidence() {
+        ObservedEntityContext entityContext = ObservedEntityContext.from(
+                ObserveEntity.builder().id(88L).type("service").name("checkout").build(),
+                List.of(
+                        EntityIdentity.builder().entityId(88L).identityKey("service.name").identityValue("checkout").build(),
+                        EntityIdentity.builder().entityId(88L).identityKey("service.namespace").identityValue("commerce").build()
+                )
+        );
+        EntityLogQueryHint queryHint = new EntityLogQueryHint(
+                "otel-resource",
+                Map.of("service.name", "checkout", "service.namespace", "commerce"),
+                Collections.emptyList(),
+                null,
+                null,
+                "checkout",
+                "commerce",
+                null,
+                1000L,
+                2000L
+        );
+        EntityLogSummaryInfo summaryInfo = new EntityLogSummaryInfo(
+                1,
+                "otel-resource",
+                "otel-resource",
+                queryHint.getResourceFilters(),
+                Collections.emptyList(),
+                "otel-resource"
+        );
+        when(logQueryRepository.queryLogs(1000L, 2000L, null, null, 20)).thenReturn(Collections.emptyList());
+
+        List<LogEvidence> evidence = telemetryIntakeService.buildLogEvidence(
+                entityContext,
+                summaryInfo,
+                List.of(queryHint)
+        );
+        EntityTriageRecommendation recommendation = telemetryIntakeService.buildTriageRecommendation(
+                new EntityEvidenceSummaryInfo(0, 0, 0, 0L, 1, System.currentTimeMillis()),
+                null,
+                summaryInfo,
+                null,
+                Collections.emptyList(),
+                evidence,
+                Collections.emptyList()
+        );
+
+        assertTrue(evidence.isEmpty());
+        assertEquals("evidence", recommendation.getRecommendedFocus());
     }
 
     @Test
@@ -571,6 +623,57 @@ class TelemetryIntakeServiceImplTest {
         assertEquals(1, summary.getTraceEvidenceCount());
     }
 
+    @Test
+    void buildTraceSummaryTreatsOtelOkAndUnsetStatusCodesAsNonErrorEvidence() {
+        ObservedEntityContext entityContext = ObservedEntityContext.from(
+                ObserveEntity.builder().id(90L).type("service").name("checkout").build(),
+                List.of(
+                        EntityIdentity.builder().entityId(90L).identityKey("service.name").identityValue("checkout").build(),
+                        EntityIdentity.builder().entityId(90L).identityKey("service.namespace").identityValue("commerce").build(),
+                        EntityIdentity.builder().entityId(90L).identityKey("deployment.environment.name").identityValue("prod").build()
+                )
+        );
+        telemetryIntakeService.recordOtlpTraceIntake(
+                Map.of(
+                        "service.name", "checkout",
+                        "service.namespace", "commerce",
+                        "deployment.environment.name", "prod"
+                ),
+                1_710_000_001_000L,
+                "trace-ok",
+                "span-ok",
+                "GET /checkout",
+                "status_code_ok",
+                Map.of("http.status_code", "200")
+        );
+        telemetryIntakeService.recordOtlpTraceIntake(
+                Map.of(
+                        "service.name", "checkout",
+                        "service.namespace", "commerce",
+                        "deployment.environment.name", "prod"
+                ),
+                1_710_000_002_000L,
+                "trace-unset",
+                "span-unset",
+                "GET /checkout/cache",
+                "status_code_unset",
+                Map.of()
+        );
+
+        EntityTraceSummaryDto traceSummary = telemetryIntakeService.buildTraceSummary(entityContext);
+        List<TraceEvidence> traceEvidence = telemetryIntakeService.buildTraceEvidence(
+                entityContext,
+                traceSummary,
+                telemetryIntakeService.buildTraceQueryHints(entityContext)
+        );
+
+        assertEquals(2, traceSummary.getRecentTraceCount());
+        assertEquals(0, traceSummary.getRecentErrorTraceCount());
+        assertEquals(1, traceEvidence.size());
+        assertEquals("ok", traceEvidence.getFirst().getErrorState());
+        assertEquals("active", traceEvidence.getFirst().getSeverityOrHealth());
+    }
+
     @Test
     void buildMetricCorrelationHintUsesCanonicalIdentityAndTimeWindow() {
         ObserveEntity entity = ObserveEntity.builder()
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java
index 4b8f92fbea..484fb99a0b 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java
@@ -68,7 +68,7 @@ class TraceQueryControllerTest {
         );
         when(entityTraceQueryService.queryTraceList(
                 1L, 100L, 200L, "trace-1", true, "checkout", "commerce", "prod",
-                "service.version=1.2.3 and hertzbeat.entity_id=\"1\" and hertzbeat.entity_type=\"service\"", "GET /checkout",
+                "service.version=1.2.3 and hertzbeat.entity_type=\"service\"", "GET /checkout",
                 100L, 500L, 2, 50, true, null, "http.route CONTAINS checkout"))
                 .thenReturn(new PageImpl<>(List.of(item), PageRequest.of(2, 50), 1));
 
@@ -97,7 +97,7 @@ class TraceQueryControllerTest {
 
         verify(entityTraceQueryService).queryTraceList(
                 1L, 100L, 200L, "trace-1", true, "checkout", "commerce", "prod",
-                "service.version=1.2.3 and hertzbeat.entity_id=\"1\" and hertzbeat.entity_type=\"service\"", "GET /checkout",
+                "service.version=1.2.3 and hertzbeat.entity_type=\"service\"", "GET /checkout",
                 100L, 500L, 2, 50, true, null, "http.route CONTAINS checkout");
     }
 
@@ -144,7 +144,7 @@ class TraceQueryControllerTest {
         TraceOverviewDto overview = new TraceOverviewDto(2, 1, 1_710_000_000_000L, true);
         when(entityTraceQueryService.getTraceOverview(
                 9L, 100L, 200L, "trace-9", false, "payments", "core", "stage",
-                "service.version=2.0.0 and hertzbeat.entity_id=\"9\"", "POST /pay", 200L, 900L, true, null, null))
+                "service.version=2.0.0", "POST /pay", 200L, 900L, true, null, null))
                 .thenReturn(overview);
 
         mockMvc.perform(get("/api/traces/stats/overview")
@@ -169,7 +169,7 @@ class TraceQueryControllerTest {
 
         verify(entityTraceQueryService).getTraceOverview(
                 9L, 100L, 200L, "trace-9", false, "payments", "core", "stage",
-                "service.version=2.0.0 and hertzbeat.entity_id=\"9\"", "POST /pay", 200L, 900L, true, null, null);
+                "service.version=2.0.0", "POST /pay", 200L, 900L, true, null, null);
     }
 
     @Test
@@ -213,7 +213,7 @@ class TraceQueryControllerTest {
         );
         when(entityTraceQueryService.getTraceGroupByStats(
                 3L, 100L, 200L, "trace-3", true, "checkout", "commerce", "prod",
-                "host.name=checkout-1 and hertzbeat.entity_id=\"3\"", "GET /checkout", 100L, 500L,
+                "host.name=checkout-1", "GET /checkout", 100L, 500L,
                 "resource:service.version", 7, "latency-p95-desc", 5, true, "entrypoint", null))
                 .thenReturn(result);
 
@@ -247,7 +247,7 @@ class TraceQueryControllerTest {
 
         verify(entityTraceQueryService).getTraceGroupByStats(
                 3L, 100L, 200L, "trace-3", true, "checkout", "commerce", "prod",
-                "host.name=checkout-1 and hertzbeat.entity_id=\"3\"", "GET /checkout", 100L, 500L,
+                "host.name=checkout-1", "GET /checkout", 100L, 500L,
                 "resource:service.version", 7, "latency-p95-desc", 5, true, "entrypoint", null);
     }
 }
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 5bc8e48704..6ba71b6977 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
@@ -161,6 +161,49 @@ class EntityTraceQueryServiceImplTest {
                 org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), org.mockito.ArgumentMatchers.any());
     }
 
+    @Test
+    void buildEntityTraceSummaryUsesResourceIdentityWhenEntityWasCreatedAfterOtlpTrace() {
+        long now = System.currentTimeMillis();
+        ObservedEntityContext entityContext = ObservedEntityContext.from(
+                ObserveEntity.builder().id(658273243069696L).type("service")
+                        .name("codex-pd-1369-novice-checkout").build(),
+                List.of(
+                        identity(658273243069696L, "service.name", "codex-pd-1369-novice-checkout", 90, true),
+                        identity(658273243069696L, "service.namespace", "product-design-1369", 30, false),
+                        identity(658273243069696L, "deployment.environment.name", "prod", 20, false)
+                ));
+        when(traceQueryRepository.supportsTraceSummaryRows()).thenReturn(true);
+        when(traceQueryRepository.queryTraceSummaryRows(
+                org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                eq("codex-pd-1369-novice-checkout"), eq("product-design-1369"), eq("prod"),
+                org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                eq(false)))
+                .thenReturn(Map.of(
+                        "total_trace_count", 1L,
+                        "error_trace_count", 0L,
+                        "latest_observed_at", now - 20_000,
+                        "latest_trace_id", "13691369136913691369136913691369"
+                ));
+
+        EntityTraceSummaryDto summary = entityTraceQueryService.buildEntityTraceSummary(entityContext);
+
+        assertEquals(1, summary.getRecentTraceCount());
+        assertEquals(0, summary.getRecentErrorTraceCount());
+        assertEquals("13691369136913691369136913691369", summary.getLatestTraceId());
+        ArgumentCaptor<Map<String, Set<String>>> identityFilterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryTraceSummaryRows(
+                org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                eq("codex-pd-1369-novice-checkout"), eq("product-design-1369"), eq("prod"),
+                org.mockito.ArgumentMatchers.isNull(), identityFilterCaptor.capture(), eq(false));
+        assertNull(identityFilterCaptor.getValue().get("hertzbeat.entity_id"));
+        assertEquals(Set.of("codex-pd-1369-novice-checkout"),
+                identityFilterCaptor.getValue().get("service.name"));
+        assertEquals(Set.of("product-design-1369"),
+                identityFilterCaptor.getValue().get("service.namespace"));
+        assertEquals(Set.of("prod"),
+                identityFilterCaptor.getValue().get("deployment.environment.name"));
+    }
+
     @Test
     void getTraceGroupByStatsUsesStorageRowsAndMapsLatencyMetrics() {
         AuthTokenRequestContext.bindWorkspaceId("team-a");
diff --git a/web-next/app/actions/actions-page.tsx b/web-next/app/actions/actions-page.tsx
index ebd38aec93..eff0744bc0 100644
--- a/web-next/app/actions/actions-page.tsx
+++ b/web-next/app/actions/actions-page.tsx
@@ -47,6 +47,14 @@ type ApprovalDraftQueueResult = {
   drafts: ApprovalDraftResult[];
 };
 
+function buildApprovalDecisionRequestPreview(draftId: string, decision?: string) {
+  return JSON.stringify({
+    draftId,
+    decision: decision === 'approved' || decision === 'rejected' ? decision : 'manual-choice-required',
+    executionAllowed: false
+  });
+}
+
 export default function ActionsPage({ suggestionContext }: { suggestionContext?: ActionSuggestionContext } = {}) {
   const { t } = useI18n();
   const state = buildActionsPlaceholderState(t, suggestionContext);
@@ -209,6 +217,10 @@ export default function ActionsPage({ suggestionContext }: { suggestionContext?:
   const approvalDecisionEndpoint = approvalDraftResult?.draftId
     ? state.approvalDecision.endpointTemplate.replace(':draftId', encodeURIComponent(approvalDraftResult.draftId))
     : state.approvalDecision.endpointTemplate;
+  const approvalDecisionRequestPreview = approvalDraftResult?.draftId
+    ? buildApprovalDecisionRequestPreview(approvalDraftResult.draftId, approvalDecisionResult?.decision)
+    : state.approvalDecision.requestPreview;
+  const canDecideApprovalDraft = Boolean(approvalDraftResult?.draftId) && approvalDecisionStatus === 'ready';
 
   return (
     <main
@@ -273,14 +285,10 @@ export default function ActionsPage({ suggestionContext }: { suggestionContext?:
             endpoint: approvalDecisionEndpoint,
             managerBacked: Boolean(approvalDecisionResult?.managerBacked),
             requestPreview: approvalDraftResult?.draftId
-              ? JSON.stringify({
-                draftId: approvalDraftResult.draftId,
-                decision: 'approved',
-                executionAllowed: false
-              })
+              ? approvalDecisionRequestPreview
               : state.approvalDecision.requestPreview,
-            onApprove: approvalDraftResult?.draftId ? () => void decideApprovalDraft('approved') : undefined,
-            onReject: approvalDraftResult?.draftId ? () => void decideApprovalDraft('rejected') : undefined,
+            onApprove: canDecideApprovalDraft ? () => void decideApprovalDraft('approved') : undefined,
+            onReject: canDecideApprovalDraft ? () => void decideApprovalDraft('rejected') : undefined,
             result: approvalDecisionResult,
             error: approvalDecisionError
           }}
diff --git a/web-next/app/actions/page.test.tsx b/web-next/app/actions/page.test.tsx
index 6a41ccce09..7cb7c161ab 100644
--- a/web-next/app/actions/page.test.tsx
+++ b/web-next/app/actions/page.test.tsx
@@ -94,6 +94,9 @@ describe('actions page', () => {
     expect(source).toContain('fetch(state.approvalDraft.endpoint');
     expect(source).toContain('fetch(state.approvalDraftQueue.endpoint');
     expect(source).toContain('fetch(endpoint');
+    expect(source).toContain('buildApprovalDecisionRequestPreview');
+    expect(source).toContain("approvalDecisionResult?.decision");
+    expect(source).not.toContain("decision: 'approved',");
     expect(source).not.toContain('rounded-[16px]');
     expect(source).not.toContain('rounded-[14px]');
     expect(source).not.toContain('#4f6cff');
@@ -159,6 +162,23 @@ describe('actions page', () => {
     expect(html).not.toContain('/actions/run');
   });
 
+  it('does not enable suggested actions from route tracking source params alone', async () => {
+    const { default: ActionsPage } = await import('./actions-page');
+    const html = renderToStaticMarkup(
+      <ActionsPage
+        suggestionContext={{
+          source: 'product-design-1590-default'
+        }}
+      />
+    );
+
+    expect(html).not.toContain('data-actions-suggested-remediation="alert-context-human-confirmation"');
+    expect(html).toContain('data-actions-approval-draft-state="awaiting-context"');
+    expect(html).toContain('data-actions-approval-draft-status="blocked"');
+    expect(html).toContain(t('actions.approval-draft.disabled'));
+    expect(html).not.toContain('&quot;actionId&quot;:&quot;suggest-restart-checkout&quot;');
+  });
+
   it('renders entity-id-only suggested remediation targets with localized fallback copy', async () => {
     const { default: ActionsPage } = await import('./actions-page');
     const html = renderToStaticMarkup(
diff --git a/web-next/app/alert-family.chrome.test.ts b/web-next/app/alert-family.chrome.test.ts
index e9176daff7..f73530dada 100644
--- a/web-next/app/alert-family.chrome.test.ts
+++ b/web-next/app/alert-family.chrome.test.ts
@@ -5,13 +5,14 @@ import { describe, expect, it } from 'vitest';
 describe('alert family cold-matte routes', () => {
   it('keeps the alert integration provider route on the HertzBeat cold source-doc shell', () => {
     const integrationSource = readFileSync(resolve(process.cwd(), 'app/alert/integration/[source]/page.tsx'), 'utf8');
+    const sourceDocShellSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/source-doc-shell.tsx'), 'utf8');
 
     expect(integrationSource).toContain('AlertIntegrationMarkdown');
-    expect(integrationSource).toContain('hzOpsCatalogVisual');
-    expect(integrationSource).toContain('data-alert-integration-surface="otlp-cold-source-doc"');
-    expect(integrationSource).toContain('data-alert-integration-container="cold-source-doc-shell"');
-    expect(integrationSource).toContain('data-alert-integration-source-rail="cold-source-list"');
-    expect(integrationSource).toContain('data-alert-integration-doc-panel="cold-markdown-doc"');
+    expect(integrationSource).toContain('HzSourceDocShell');
+    expect(integrationSource).toContain('data-alert-integration-surface="hertzbeat-ui-source-doc"');
+    expect(integrationSource).toContain('data-alert-integration-shell-owner="hertzbeat-ui-source-doc-shell"');
+    expect(sourceDocShellSource).toContain('data-hz-source-doc-rail-owner="hertzbeat-ui-source-doc-shell"');
+    expect(sourceDocShellSource).toContain('data-hz-source-doc-panel-owner="hertzbeat-ui-source-doc-shell"');
     expect(integrationSource).not.toContain('components/observability');
     expect(integrationSource).not.toContain('components/workbench/primitives');
     expect(integrationSource).not.toContain('components/observability/code-pane');
@@ -19,15 +20,17 @@ describe('alert family cold-matte routes', () => {
 
   it('keeps the alert notice route on the HertzBeat notification-closure workbench shell', () => {
     const noticeSource = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
+    const noticePageSource = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
     const noticeShellSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-console-shell.tsx'), 'utf8');
 
-    expect(noticeSource).toContain('AlertNoticeConsoleShell');
-    expect(noticeSource).toContain('hzOpsCatalogVisual');
-    expect(noticeSource).toContain('data-alert-notice-surface="otlp-cold-notice-console"');
-    expect(noticeSource).toContain('data-alert-notice-header="cold-compact-header"');
-    expect(noticeSource).toContain('HzConfirmDialog');
-    expect(noticeShellSource).toContain('data-alert-notice-workbench-panel="cold-tabbed-table-panel"');
-    expect(noticeShellSource).toContain('data-alert-notice-global-panel="cold-matte-tabbed-table"');
+    expect(noticeSource).toContain('AlertNoticePage');
+    expect(noticePageSource).toContain('AlertNoticeConsoleShell');
+    expect(noticePageSource).toContain('hzOpsCatalogVisual');
+    expect(noticePageSource).toContain('data-alert-notice-surface="otlp-hertzbeat-ui-notice-console"');
+    expect(noticePageSource).toContain('data-alert-notice-header="hertzbeat-ui-compact-header"');
+    expect(noticePageSource).toContain('HzConfirmDialog');
+    expect(noticeShellSource).toContain('data-alert-notice-workbench-panel="hertzbeat-ui-tabbed-table-panel"');
+    expect(noticeShellSource).toContain('data-alert-notice-global-panel="hertzbeat-ui-matte-tabbed-table"');
     expect(noticeSource).not.toContain('components/observability');
     expect(noticeSource).not.toContain('components/workbench/primitives');
   });
diff --git a/web-next/app/alert/alert-center-page.tsx b/web-next/app/alert/alert-center-page.tsx
index f666601e3c..59ecb3fe0d 100644
--- a/web-next/app/alert/alert-center-page.tsx
+++ b/web-next/app/alert/alert-center-page.tsx
@@ -17,6 +17,7 @@ import {
 } from '../../lib/alert-manage/controller';
 import {
   ALERT_CENTER_PAGE_SIZE_OPTIONS,
+  buildAlertCenterRouteUrl,
   buildAlertListUrl,
   hasAlertEntityContext,
   type AlertCenterRouteState,
@@ -149,19 +150,23 @@ export default function AlertCenterPage({ initialRouteState }: { initialRouteSta
 
   const handlePageIndexChange = useCallback((nextPageIndex: number) => {
     const normalizedPageIndex = Math.max(0, Math.floor(nextPageIndex));
+    const nextQuery = { ...query, pageIndex: normalizedPageIndex };
     setDraft(current => ({ ...current, pageIndex: normalizedPageIndex }));
-    setQuery(current => ({ ...current, pageIndex: normalizedPageIndex }));
+    setQuery(nextQuery);
+    router.replace(buildAlertCenterRouteUrl(nextQuery));
     setSelectedGroupIds([]);
-  }, []);
+  }, [query, router]);
 
   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];
+    const nextQuery = { ...query, pageIndex: 0, pageSize: normalizedPageSize };
     setDraft(current => ({ ...current, pageIndex: 0, pageSize: normalizedPageSize }));
-    setQuery(current => ({ ...current, pageIndex: 0, pageSize: normalizedPageSize }));
+    setQuery(nextQuery);
+    router.replace(buildAlertCenterRouteUrl(nextQuery));
     setSelectedGroupIds([]);
-  }, []);
+  }, [query, router]);
 
   const handleClosureAction = useCallback(async (action: AlertClosureOperationAction, groupId: number | number[], totalElements = 0) => {
     setOperationFeedback(null);
@@ -178,8 +183,10 @@ export default function AlertCenterPage({ initialRouteState }: { initialRouteSta
           ? clampAlertCenterPageIndexAfterDelete(nextQuery, totalElements, affectedCount)
           : nextQuery;
       };
+      const nextRouteQuery = buildPostActionQuery(query);
       setDraft(current => buildPostActionQuery(current));
       setQuery(current => buildPostActionQuery(current));
+      router.replace(buildAlertCenterRouteUrl(nextRouteQuery));
       setSelectedGroupIds([]);
       setRefreshNonce(current => current + 1);
     } catch (error) {
@@ -188,7 +195,7 @@ export default function AlertCenterPage({ initialRouteState }: { initialRouteSta
         copy: buildAlertClosureOperationFailureFeedback(action, t)
       });
     }
-  }, [query, t]);
+  }, [query, router, t]);
 
   const handleRuleQuickCreate = useCallback(async (
     mode: AlertRuleDialogMode,
@@ -244,6 +251,7 @@ export default function AlertCenterPage({ initialRouteState }: { initialRouteSta
             setQuery(cleared);
             setSelectedGroupIds([]);
             setEntityResponseResult(null);
+            router.replace(buildAlertCenterRouteUrl(cleared));
           }}
           operationFeedback={operationFeedback}
           entityResponseResult={entityResponseResult}
diff --git a/web-next/app/alert/group/alert-group-page.tsx b/web-next/app/alert/group/alert-group-page.tsx
index 0f44286e02..9273e5d47c 100644
--- a/web-next/app/alert/group/alert-group-page.tsx
+++ b/web-next/app/alert/group/alert-group-page.tsx
@@ -1,6 +1,7 @@
 'use client';
 
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter, 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';
@@ -17,7 +18,13 @@ import {
   type AlertGroupFormDraft
 } from '../../../lib/alert-group/controller';
 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 {
+  buildAlertGroupEvidenceContext,
+  buildAlertGroupFormDraft,
+  getAlertGroupValidationField,
+  validateAlertGroupForm,
+  type AlertGroupValidationField
+} from '../../../lib/alert-group/view-model';
 import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptionsFromFacade } from '../../../lib/alert-label-options';
 import { formatTime } from '../../../lib/format';
 import type { AlertGroupConverge } from '../../../lib/types';
@@ -34,19 +41,75 @@ const EMPTY_ALERT_GROUP_ROUTE_STATE: AlertGroupRouteState = {
   signal: null,
   signalContext: {}
 };
+const ALERT_GROUP_ROUTE_PATH = '/alert/group';
+const ALERT_GROUP_EDITOR_FOCUS_SELECTORS: Record<AlertGroupValidationField, string> = {
+  name: 'input[name="alert_group_name"]',
+  'group-labels': '[data-alert-group-label-selector] input[data-hz-tag-input-control="draft"]',
+  'group-wait': 'input[name="alert_group_wait"]',
+  'group-interval': 'input[name="alert_group_interval"]',
+  'repeat-interval': 'input[name="alert_group_repeat_interval"]'
+};
+const ALERT_GROUP_DRAFT_FINGERPRINT_FIELDS: Array<keyof AlertGroupFormDraft> = [
+  'id',
+  'name',
+  'enable',
+  'groupLabelsText',
+  'groupWait',
+  'groupInterval',
+  'repeatInterval'
+];
+
+type AlertGroupListRouteState = {
+  search: string;
+  pageIndex: number;
+  pageSize: number;
+};
+
+function parseAlertGroupRouteInteger(value: string | null, fallback: number, minimum = 0) {
+  if (!value) return fallback;
+  const parsed = Number.parseInt(value, 10);
+  return Number.isFinite(parsed) && parsed >= minimum ? parsed : fallback;
+}
+
+function focusAlertGroupEditorField(field: AlertGroupValidationField) {
+  if (typeof document === 'undefined') return;
+  const selector = ALERT_GROUP_EDITOR_FOCUS_SELECTORS[field];
+  window.requestAnimationFrame(() => {
+    const target = document.querySelector<HTMLInputElement>(selector);
+    target?.focus();
+    target?.scrollIntoView({ block: 'center', inline: 'nearest' });
+  });
+}
+
+function serializeAlertGroupDraft(draft: AlertGroupFormDraft) {
+  return JSON.stringify(
+    ALERT_GROUP_DRAFT_FINGERPRINT_FIELDS.map(field => [field, draft[field] == null ? '' : String(draft[field]).trim()])
+  );
+}
 
 export default function AlertGroupPage({ initialRouteState }: { initialRouteState?: AlertGroupRouteState } = {}) {
   const { t } = useI18n();
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const routeSearchParamString = searchParams.toString();
+  const routeSearch = searchParams.get('search') ?? '';
+  const routePageIndex = parseAlertGroupRouteInteger(searchParams.get('pageIndex'), 0);
+  const routePageSize = parseAlertGroupRouteInteger(searchParams.get('pageSize'), ALERT_GROUP_PAGE_SIZE_OPTIONS[0], 1);
+  const routeListState = useMemo<AlertGroupListRouteState>(() => ({
+    search: routeSearch,
+    pageIndex: routePageIndex,
+    pageSize: routePageSize
+  }), [routePageIndex, routePageSize, routeSearch]);
   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 [search, setSearch] = useState(routeListState.search);
+  const [query, setQuery] = useState(routeListState.search);
+  const [pageIndex, setPageIndex] = useState(routeListState.pageIndex);
+  const [pageSize, setPageSize] = useState<number>(routeListState.pageSize);
   const [selectedId, setSelectedId] = useState<number | null>(null);
   const [editorOpen, setEditorOpen] = useState(false);
   const [editorLoading, setEditorLoading] = useState(false);
@@ -56,6 +119,10 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
   const [editorErrorDetail, setEditorErrorDetail] = useState<string | null>(null);
   const [editorErrorContract, setEditorErrorContract] = useState<AlertGroupErrorContract | null>(null);
   const [draft, setDraft] = useState<AlertGroupFormDraft>(() => buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch));
+  const [editorInitialFingerprint, setEditorInitialFingerprint] = useState(() => (
+    serializeAlertGroupDraft(buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch))
+  ));
+  const [editorDiscardDialogOpen, setEditorDiscardDialogOpen] = useState(false);
   const [refreshTick, setRefreshTick] = useState(0);
   const [checkedIds, setCheckedIds] = useState<number[]>([]);
   const [deleteRequest, setDeleteRequest] = useState<GroupDeleteRequest | null>(null);
@@ -65,6 +132,46 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
     () => ['alert-group', alertGroupListUrl, refreshTick].join('|'),
     [alertGroupListUrl, refreshTick]
   );
+  const editorDraftFingerprint = useMemo(() => serializeAlertGroupDraft(draft), [draft]);
+  const shouldConfirmEditorDiscard = Boolean(editorOpen && editorDraftFingerprint !== editorInitialFingerprint && !editorSaving);
+
+  useEffect(() => {
+    setSearch(routeListState.search);
+    setQuery(routeListState.search);
+    setPageIndex(routeListState.pageIndex);
+    setPageSize(routeListState.pageSize);
+    setSelectedId(null);
+    setCheckedIds([]);
+  }, [routeListState]);
+
+  const replaceRouteQuery = useCallback((nextState: AlertGroupListRouteState) => {
+    const nextParams = new URLSearchParams(routeSearchParamString);
+    const cleanSearch = nextState.search.trim();
+    if (cleanSearch) {
+      nextParams.set('search', cleanSearch);
+    } else {
+      nextParams.delete('search');
+    }
+
+    if (nextState.pageIndex > 0) {
+      nextParams.set('pageIndex', String(nextState.pageIndex));
+    } else {
+      nextParams.delete('pageIndex');
+    }
+
+    if (nextState.pageSize !== ALERT_GROUP_PAGE_SIZE_OPTIONS[0]) {
+      nextParams.set('pageSize', String(nextState.pageSize));
+    } else {
+      nextParams.delete('pageSize');
+    }
+
+    const nextParamString = nextParams.toString();
+    const nextUrl = nextParamString ? `${ALERT_GROUP_ROUTE_PATH}?${nextParamString}` : ALERT_GROUP_ROUTE_PATH;
+    const currentUrl = routeSearchParamString ? `${ALERT_GROUP_ROUTE_PATH}?${routeSearchParamString}` : ALERT_GROUP_ROUTE_PATH;
+    if (nextUrl !== currentUrl) {
+      router.replace(nextUrl, { scroll: false });
+    }
+  }, [routeSearchParamString, router]);
 
   const load = useCallback(async () => {
     const data = await loadAlertGroupDataFromFacade(
@@ -89,7 +196,10 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
         const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
 
         async function handleNew() {
-          setDraft(buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch));
+          const nextDraft = buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch);
+          setDraft(nextDraft);
+          setEditorInitialFingerprint(serializeAlertGroupDraft(nextDraft));
+          setEditorDiscardDialogOpen(false);
           setEditorError(null);
           setEditorErrorDetail(null);
           setEditorErrorContract(null);
@@ -107,7 +217,10 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
           setEditorMessage(null);
           try {
             const detail = await loadAlertGroupDetailFromFacade(api.alertGroups.detail, targetId);
-            setDraft(buildAlertGroupFormDraft(detail));
+            const nextDraft = buildAlertGroupFormDraft(detail);
+            setDraft(nextDraft);
+            setEditorInitialFingerprint(serializeAlertGroupDraft(nextDraft));
+            setEditorDiscardDialogOpen(false);
             setEditorOpen(true);
           } catch (error) {
             setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
@@ -121,9 +234,13 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
         async function handleSave() {
           const validationError = validateAlertGroupForm(draft, t);
           if (validationError) {
+            const validationField = getAlertGroupValidationField(draft);
             setEditorError(validationError);
             setEditorErrorDetail(null);
             setEditorErrorContract(null);
+            if (validationField) {
+              focusAlertGroupEditorField(validationField);
+            }
             return;
           }
           const isEdit = Boolean(draft.id);
@@ -138,8 +255,10 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
             } else {
               await createAlertGroupFromFacade(api.alertGroups.create, draft);
             }
+            setEditorInitialFingerprint(serializeAlertGroupDraft(draft));
             setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditorOpen(false);
+            setEditorDiscardDialogOpen(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
             setEditorError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
@@ -223,33 +342,77 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
         }
 
         function handleApplyFilter() {
-          setQuery(search);
+          const nextSearch = search.trim();
+          const nextState = { search: nextSearch, pageIndex: 0, pageSize };
+          setSearch(nextSearch);
+          setQuery(nextSearch);
           setPageIndex(0);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery(nextState);
         }
 
         function handleClearFilter() {
+          const nextState = { search: '', pageIndex: 0, pageSize };
           setSearch('');
           setQuery('');
           setPageIndex(0);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery(nextState);
         }
 
         function handlePageIndexChange(nextPageIndex: number) {
           setPageIndex(nextPageIndex);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery({ search: query, pageIndex: nextPageIndex, pageSize });
         }
 
         function handlePageSizeChange(nextPageSize: number) {
+          const nextState = { search: query, pageIndex: 0, pageSize: nextPageSize };
           setPageSize(nextPageSize);
           setPageIndex(0);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery(nextState);
+        }
+
+        function handleCloseEditor() {
+          setEditorOpen(false);
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorMessage(null);
+          setEditorDiscardDialogOpen(false);
         }
 
+        function requestCloseEditor() {
+          if (shouldConfirmEditorDiscard) {
+            setEditorDiscardDialogOpen(true);
+            return;
+          }
+          handleCloseEditor();
+        }
+
+        const deleteTargetNames = deleteRequest?.ids
+          .map(id => data.list.content.find(item => item.id === id)?.name?.trim())
+          .filter((name): name is string => Boolean(name)) ?? [];
+        const missingDeleteTargetCount = deleteRequest
+          ? Math.max(deleteRequest.ids.length - deleteTargetNames.length, 0)
+          : 0;
+        const deleteConfirmCopy = [
+          deleteRequest?.kind === 'batch'
+            ? t('alert.group.delete.confirm.batch', { count: deleteRequest.ids.length })
+            : t('alert.group.delete.confirm.single'),
+          deleteTargetNames.length > 0
+            ? t('alert.group.delete.confirm.targets', { names: deleteTargetNames.join(', ') })
+            : null,
+          missingDeleteTargetCount > 0
+            ? t('alert.group.delete.confirm.targets-more', { count: missingDeleteTargetCount })
+            : null
+        ].filter(Boolean).join('\n');
+
         return (
           <>
             <AlertGroupSurface
@@ -258,6 +421,7 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
               search={search}
               selectedId={selectedId}
               checkedIds={checkedIds}
+              requestedPageSize={pageSize}
               editorOpen={editorOpen}
               editorLoading={editorLoading}
               editorSaving={editorSaving}
@@ -284,19 +448,30 @@ export default function AlertGroupPage({ initialRouteState }: { initialRouteStat
               onEdit={groupId => void handleEdit(groupId)}
               onDelete={groupId => void handleDelete(groupId)}
               onDeleteSelected={() => void handleDeleteSelected()}
-              onCloseEditor={() => setEditorOpen(false)}
+              onCloseEditor={requestCloseEditor}
               onDraftChange={setDraft}
             />
+            <div
+              data-alert-group-unsaved-cancel="hertzbeat-ui-confirm-dialog"
+              data-alert-group-unsaved-cancel-state={editorDiscardDialogOpen ? 'open' : 'closed'}
+            >
+              <HzConfirmDialog
+                open={editorDiscardDialogOpen}
+                title={t('alert.group.unsaved-cancel.title')}
+                kicker={t('alert.group.unsaved-cancel.kicker')}
+                copy={t('alert.group.unsaved-cancel.copy')}
+                confirmLabel={t('alert.group.unsaved-cancel.discard')}
+                cancelLabel={t('alert.group.unsaved-cancel.keep-editing')}
+                onCancel={() => setEditorDiscardDialogOpen(false)}
+                onConfirm={handleCloseEditor}
+              />
+            </div>
             <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
               <HzConfirmDialog
                 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')}
+                copy={deleteConfirmCopy}
+                confirmLabel={t('alert.group.delete.confirm.action')}
                 cancelLabel={t('common.button.cancel')}
                 pending={deletePending}
                 onCancel={() => setDeleteRequest(null)}
diff --git a/web-next/app/alert/group/page.test.tsx b/web-next/app/alert/group/page.test.tsx
index ba0c09dc7a..c419ecad65 100644
--- a/web-next/app/alert/group/page.test.tsx
+++ b/web-next/app/alert/group/page.test.tsx
@@ -1,13 +1,20 @@
+// @vitest-environment jsdom
+
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import React from 'react';
+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';
 import type { AlertGroupRouteState } from '../../../lib/alert-group/query-state';
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
+  lastSurfaceProps: null as null | Record<string, any>,
+  currentSearchParams: '',
+  routerReplace: vi.fn(),
   renderData: {
     list: {
       totalElements: 1,
@@ -37,6 +44,16 @@ const mockState = vi.hoisted(() => ({
 
 const apiMessageGet = vi.hoisted(() => vi.fn());
 
+(globalThis as { React?: typeof React }).React = React;
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: mockState.routerReplace
+  }),
+  useSearchParams: () => new URLSearchParams(mockState.currentSearchParams)
+}));
+
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock()
@@ -63,31 +80,32 @@ vi.mock('../../../components/workbench/client-workbench', () => ({
 }));
 
 vi.mock('../../../components/pages/alert-group-surface', () => ({
-  AlertGroupSurface: ({
-    data,
-    labelOptions,
-    evidenceContext,
-    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 ?? ''}
-      data-alert-group-prefill-labels={evidenceContext?.groupLabelsText ?? ''}
-      data-alert-group-draft-labels={draft?.groupLabelsText ?? ''}
-    />
-  )
+  AlertGroupSurface: (props: any) => {
+    const {
+      data,
+      labelOptions,
+      evidenceContext,
+      draft,
+      pageSizeOptions,
+      search
+    } = props;
+    mockState.lastSurfaceProps = props;
+    return (
+      <div
+        data-alert-group-surface="true"
+        data-total={data.list.totalElements}
+        data-label-options={labelOptions?.keys?.join('|')}
+        data-page-size-options={pageSizeOptions?.join('|')}
+        data-requested-page-size={props.requestedPageSize}
+        data-search={search}
+        data-alert-group-evidence-context={evidenceContext ? 'signal-route' : 'none'}
+        data-alert-group-evidence-signal={evidenceContext?.signal ?? ''}
+        data-alert-group-evidence-return={evidenceContext?.returnHref ?? ''}
+        data-alert-group-prefill-labels={evidenceContext?.groupLabelsText ?? ''}
+        data-alert-group-draft-labels={draft?.groupLabelsText ?? ''}
+      />
+    );
+  }
 }));
 
 vi.mock('../../../lib/api-client', () => ({
@@ -108,8 +126,25 @@ async function renderAlertGroupPage(initialRouteState: AlertGroupRouteState = EM
 }
 
 describe('alert group page', () => {
+  let interactionContainer: HTMLDivElement | null = null;
+  let interactionRoot: Root | null = null;
+
+  afterEach(() => {
+    if (interactionRoot) {
+      act(() => {
+        interactionRoot?.unmount();
+      });
+    }
+    interactionRoot = null;
+    interactionContainer?.remove();
+    interactionContainer = null;
+  });
+
   beforeEach(() => {
     mockState.lastLoad = null;
+    mockState.lastSurfaceProps = null;
+    mockState.currentSearchParams = '';
+    mockState.routerReplace.mockReset();
     apiMessageGet.mockClear().mockResolvedValue(mockState.renderData.list);
   });
 
@@ -119,6 +154,7 @@ describe('alert group page', () => {
     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-search=""');
     expect(html).toContain('data-loading-copy="Loading group rules"');
 
     await mockState.lastLoad?.();
@@ -157,7 +193,7 @@ describe('alert group page', () => {
     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).toContain("import { useRouter, useSearchParams } from 'next/navigation';");
     expect(source).not.toContain('readSignalRouteContext(searchParams)');
     expect(source).toContain('const alertGroupRouteState = initialRouteState ?? EMPTY_ALERT_GROUP_ROUTE_STATE');
   });
@@ -167,8 +203,8 @@ describe('alert group page', () => {
 
     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('const [pageIndex, setPageIndex] = useState(routeListState.pageIndex)');
+    expect(source).toContain('const [pageSize, setPageSize] = useState<number>(routeListState.pageSize)');
     expect(source).toContain("['alert-group', alertGroupListUrl, refreshTick].join('|')");
     expect(source).toContain('buildAlertGroupUrl({ search: query, pageIndex, pageSize })');
     expect(source).toContain('[pageIndex, pageSize, query]');
@@ -187,6 +223,73 @@ describe('alert group page', () => {
     expect(source).toContain('setPageIndex(0);');
   });
 
+  it('initializes alert group list state from the route and preserves URL state during search and pagination', async () => {
+    mockState.currentSearchParams = 'search=ops&pageIndex=2&pageSize=15&signal=metrics&entityId=7';
+    const { default: AlertGroupPage } = await import('./alert-group-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<AlertGroupPage />);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.search).toBe('ops');
+    expect(mockState.lastSurfaceProps?.requestedPageSize).toBe(15);
+    await act(async () => {
+      await mockState.lastLoad?.();
+    });
+    expect(apiMessageGet).toHaveBeenCalledWith('/alert/groups?pageIndex=2&pageSize=15&sort=id&order=desc&search=ops');
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSearchChange('checkout');
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onApplyFilter();
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/group?search=checkout&pageSize=15&signal=metrics&entityId=7', { scroll: false });
+
+    mockState.currentSearchParams = 'search=checkout&pageSize=15&signal=metrics&entityId=7';
+    await act(async () => {
+      interactionRoot?.render(<AlertGroupPage />);
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onPageIndexChange(3);
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/group?search=checkout&pageSize=15&signal=metrics&entityId=7&pageIndex=3', { scroll: false });
+
+    mockState.currentSearchParams = 'search=checkout&pageSize=15&signal=metrics&entityId=7';
+    await act(async () => {
+      interactionRoot?.render(<AlertGroupPage />);
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onPageSizeChange(8);
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/group?search=checkout&signal=metrics&entityId=7', { scroll: false });
+
+    mockState.currentSearchParams = 'search=checkout&pageSize=15&signal=metrics&entityId=7';
+    await act(async () => {
+      interactionRoot?.render(<AlertGroupPage />);
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onClearFilter();
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/group?signal=metrics&entityId=7', { scroll: false });
+  });
+
   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');
 
@@ -220,6 +323,55 @@ describe('alert group page', () => {
     expect(source).not.toContain("t('common.save-failed')");
   });
 
+  it('clears stale editor validation when the group editor is closed', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('function handleCloseEditor()');
+    expect(source).toContain('setEditorOpen(false);');
+    expect(source).toContain('setEditorError(null);');
+    expect(source).toContain('setEditorErrorDetail(null);');
+    expect(source).toContain('setEditorErrorContract(null);');
+    expect(source).toContain('setEditorMessage(null);');
+    expect(source).toContain('function requestCloseEditor()');
+    expect(source).toContain('setEditorDiscardDialogOpen(true)');
+    expect(source).toContain('onCloseEditor={requestCloseEditor}');
+    expect(source).not.toContain('onCloseEditor={() => setEditorOpen(false)}');
+  });
+
+  it('asks for confirmation before closing a dirty group editor draft', async () => {
+    const { default: AlertGroupPage } = await import('./alert-group-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<AlertGroupPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onNew();
+      await Promise.resolve();
+    });
+
+    const draft = mockState.lastSurfaceProps?.draft;
+    expect(mockState.lastSurfaceProps?.editorOpen).toBe(true);
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDraftChange({ ...draft, name: 'Unsaved group draft' });
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onCloseEditor();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.editorOpen).toBe(true);
+    expect(interactionContainer.innerHTML).toContain('data-alert-group-unsaved-cancel-state="open"');
+    expect(interactionContainer.innerHTML).toContain('Discard unsaved grouping changes?');
+  });
+
   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');
 
@@ -234,11 +386,21 @@ describe('alert group page', () => {
     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("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("t('alert.group.validation.group-interval')");
     expect(source).toContain('if (!draft.repeatInterval.trim())');
-    expect(source).toContain("return t('alert.group.validation.repeat-interval')");
+    expect(source).toContain("t('alert.group.validation.repeat-interval')");
+  });
+
+  it('focuses the first invalid editor field after a failed novice save attempt', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('ALERT_GROUP_EDITOR_FOCUS_SELECTORS');
+    expect(source).toContain("'group-labels': '[data-alert-group-label-selector] input[data-hz-tag-input-control=\"draft\"]'");
+    expect(source).toContain('function focusAlertGroupEditorField(field: AlertGroupValidationField)');
+    expect(source).toContain('const validationField = getAlertGroupValidationField(draft)');
+    expect(source).toContain('focusAlertGroupEditorField(validationField)');
   });
 
   it('validates Angular non-negative timer fields before saving alert groups', () => {
@@ -247,9 +409,9 @@ describe('alert group page', () => {
     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')");
+    expect(source).toContain("t('alert.group.validation.group-wait-non-negative')");
+    expect(source).toContain("t('alert.group.validation.group-interval-non-negative')");
+    expect(source).toContain("t('alert.group.validation.repeat-interval-non-negative')");
   });
 
   it('uses Angular delete notifications for confirmed delete feedback', () => {
@@ -264,4 +426,14 @@ describe('alert group page', () => {
     expect(source).not.toContain("setEditorMessage(t('common.delete-success'))");
     expect(source).not.toContain("t('common.delete-failed')");
   });
+
+  it('names the target and destructive action in group delete confirmation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('const deleteTargetNames = deleteRequest?.ids');
+    expect(source).toContain("t('alert.group.delete.confirm.targets', { names: deleteTargetNames.join(', ') })");
+    expect(source).toContain("t('alert.group.delete.confirm.targets-more', { count: missingDeleteTargetCount })");
+    expect(source).toContain("confirmLabel={t('alert.group.delete.confirm.action')}");
+    expect(source).not.toContain("confirmLabel={t('common.button.ok')}");
+  });
 });
diff --git a/web-next/app/alert/inhibit/alert-inhibit-page.tsx b/web-next/app/alert/inhibit/alert-inhibit-page.tsx
index d1b6c5c6de..7b21acec93 100644
--- a/web-next/app/alert/inhibit/alert-inhibit-page.tsx
+++ b/web-next/app/alert/inhibit/alert-inhibit-page.tsx
@@ -1,7 +1,8 @@
 'use client';
 
 import React from 'react';
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter, 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';
@@ -21,12 +22,18 @@ import {
   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 {
+  buildAlertInhibitEvidenceContext,
+  getAlertInhibitValidationField,
+  validateAlertInhibitForm,
+  type AlertInhibitValidationField
+} 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';
+import type { SignalRouteContext } from '../../../lib/signal-route-context';
 
 type InhibitDeleteRequest = {
   kind: 'single' | 'batch';
@@ -35,6 +42,21 @@ type InhibitDeleteRequest = {
 
 const ALERT_INHIBIT_SETTLED_CACHE_TTL_MS = 10_000;
 const ALERT_INHIBIT_LABEL_OPTIONS_TIMEOUT_MS = 2_500;
+const ALERT_INHIBIT_ROUTE_PATH = '/alert/inhibit';
+const ALERT_INHIBIT_EDITOR_FOCUS_SELECTORS: Record<AlertInhibitValidationField, string> = {
+  name: 'input[name="inhibit_name"]',
+  'source-labels': '[data-alert-inhibit-source-label-selector] [data-hz-label-selector-draft-row="true"] input[data-hz-label-selector-key-input="searchable-key"]',
+  'target-labels': '[data-alert-inhibit-target-label-selector] [data-hz-label-selector-draft-row="true"] input[data-hz-label-selector-key-input="searchable-key"]',
+  'equal-labels': '[data-alert-inhibit-equal-label-selector] input[data-hz-tag-input-control="draft"]'
+};
+const ALERT_INHIBIT_DRAFT_FINGERPRINT_FIELDS: Array<keyof AlertInhibitFormDraft> = [
+  'id',
+  'name',
+  'enable',
+  'sourceLabelsText',
+  'targetLabelsText',
+  'equalLabelsText'
+];
 const EMPTY_ALERT_INHIBIT_ROUTE_STATE: AlertInhibitRouteState = {
   returnContext: {
     search: '',
@@ -58,6 +80,18 @@ const EMPTY_ALERT_INHIBIT_ROUTE_STATE: AlertInhibitRouteState = {
   }
 };
 
+type AlertInhibitListRouteState = {
+  search: string;
+  pageIndex: number;
+  pageSize: number;
+};
+
+function parseAlertInhibitRouteInteger(value: string | null, fallback: number, minimum = 0) {
+  if (!value) return fallback;
+  const parsed = Number.parseInt(value, 10);
+  return Number.isFinite(parsed) && parsed >= minimum ? parsed : fallback;
+}
+
 function filterMatchedInhibitsBySearch(inhibits: AlertInhibit[], search: string): AlertInhibit[] {
   const keyword = search.trim().toLowerCase();
   if (!keyword) return inhibits;
@@ -105,18 +139,75 @@ function withTimeoutFallback<T>(promise: Promise<T>, fallback: T, timeoutMs: num
   });
 }
 
+function focusAlertInhibitEditorField(field: AlertInhibitValidationField) {
+  if (typeof document === 'undefined') return;
+  const selector = ALERT_INHIBIT_EDITOR_FOCUS_SELECTORS[field];
+  window.requestAnimationFrame(() => {
+    const target = document.querySelector<HTMLInputElement>(selector);
+    target?.focus();
+    target?.scrollIntoView({ block: 'center', inline: 'nearest' });
+  });
+}
+
+function serializeAlertInhibitDraft(draft: AlertInhibitFormDraft) {
+  return JSON.stringify(
+    ALERT_INHIBIT_DRAFT_FINGERPRINT_FIELDS.map(field => [field, draft[field] == null ? '' : String(draft[field]).trim()])
+  );
+}
+
+function buildAlertInhibitReturnEvidenceContext(returnContext: AlertInhibitRouteState['returnContext']): SignalRouteContext {
+  return {
+    entityId: returnContext.entityId,
+    entityName: returnContext.entityName,
+    returnTo: returnContext.returnTo,
+    serviceName: returnContext.serviceName,
+    serviceNamespace: returnContext.serviceNamespace,
+    environment: returnContext.environment,
+    timeRange: returnContext.timeRange,
+    start: returnContext.start,
+    end: returnContext.end,
+    refresh: returnContext.refresh,
+    live: returnContext.live,
+    tz: returnContext.tz,
+    source: returnContext.source,
+    monitorId: returnContext.monitorId,
+    monitorName: returnContext.monitorName,
+    monitorApp: returnContext.monitorApp,
+    monitorInstance: returnContext.monitorInstance,
+    traceId: returnContext.traceId,
+    spanId: returnContext.spanId,
+    collector: returnContext.collector,
+    template: returnContext.template
+  };
+}
+
 export default function AlertInhibitPage({ initialRouteState }: { initialRouteState?: AlertInhibitRouteState } = {}) {
   const { t } = useI18n();
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const routeSearchParamString = searchParams.toString();
+  const routeSearch = searchParams.get('search') ?? '';
+  const routePageIndex = parseAlertInhibitRouteInteger(searchParams.get('pageIndex'), 0);
+  const routePageSize = parseAlertInhibitRouteInteger(searchParams.get('pageSize'), ALERT_INHIBIT_PAGE_SIZE_OPTIONS[0], 1);
+  const routeListState = useMemo<AlertInhibitListRouteState>(() => ({
+    search: routeSearch,
+    pageIndex: routePageIndex,
+    pageSize: routePageSize
+  }), [routePageIndex, routePageSize, routeSearch]);
   const alertInhibitRouteState = initialRouteState ?? EMPTY_ALERT_INHIBIT_ROUTE_STATE;
   const { returnContext, signal, signalContext, managementContext } = alertInhibitRouteState;
+  const inhibitEvidenceRouteContext = useMemo(
+    () => signal ? signalContext : buildAlertInhibitReturnEvidenceContext(returnContext),
+    [returnContext, signal, signalContext]
+  );
   const inhibitEvidenceContext = useMemo(
-    () => buildAlertInhibitEvidenceContext(signal, signalContext, t),
-    [signal, signalContext, t]
+    () => buildAlertInhibitEvidenceContext(signal, inhibitEvidenceRouteContext, t),
+    [signal, inhibitEvidenceRouteContext, 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 [search, setSearch] = useState(routeListState.search);
+  const [query, setQuery] = useState(routeListState.search);
+  const [pageIndex, setPageIndex] = useState(routeListState.pageIndex);
+  const [pageSize, setPageSize] = useState<number>(routeListState.pageSize);
   const [selectedId, setSelectedId] = useState<number | null>(null);
   const [editorOpen, setEditorOpen] = useState(false);
   const [editorLoading, setEditorLoading] = useState(false);
@@ -126,6 +217,10 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
   const [editorErrorDetail, setEditorErrorDetail] = useState<string | null>(null);
   const [editorErrorContract, setEditorErrorContract] = useState<'save' | 'enable' | 'delete' | null>(null);
   const [draft, setDraft] = useState<AlertInhibitFormDraft>(() => buildAlertInhibitFormDraft(null, inhibitEvidenceContext?.draftPatch));
+  const [editorInitialFingerprint, setEditorInitialFingerprint] = useState(() => (
+    serializeAlertInhibitDraft(buildAlertInhibitFormDraft(null, inhibitEvidenceContext?.draftPatch))
+  ));
+  const [editorDiscardDialogOpen, setEditorDiscardDialogOpen] = useState(false);
   const [refreshTick, setRefreshTick] = useState(0);
   const [checkedIds, setCheckedIds] = useState<number[]>([]);
   const [deleteRequest, setDeleteRequest] = useState<InhibitDeleteRequest | null>(null);
@@ -141,6 +236,46 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
     () => ['alert-inhibit', useMatchedView ? `matched:${matchedRuleIdsKey}` : alertInhibitListUrl, refreshTick].join('|'),
     [alertInhibitListUrl, matchedRuleIdsKey, refreshTick, useMatchedView]
   );
+  const editorDraftFingerprint = useMemo(() => serializeAlertInhibitDraft(draft), [draft]);
+  const shouldConfirmEditorDiscard = Boolean(editorOpen && editorDraftFingerprint !== editorInitialFingerprint && !editorSaving);
+
+  useEffect(() => {
+    setSearch(routeListState.search);
+    setQuery(routeListState.search);
+    setPageIndex(routeListState.pageIndex);
+    setPageSize(routeListState.pageSize);
+    setSelectedId(null);
+    setCheckedIds([]);
+  }, [routeListState]);
+
+  const replaceRouteQuery = useCallback((nextState: AlertInhibitListRouteState) => {
+    const nextParams = new URLSearchParams(routeSearchParamString);
+    const cleanSearch = nextState.search.trim();
+    if (cleanSearch) {
+      nextParams.set('search', cleanSearch);
+    } else {
+      nextParams.delete('search');
+    }
+
+    if (nextState.pageIndex > 0) {
+      nextParams.set('pageIndex', String(nextState.pageIndex));
+    } else {
+      nextParams.delete('pageIndex');
+    }
+
+    if (nextState.pageSize !== ALERT_INHIBIT_PAGE_SIZE_OPTIONS[0]) {
+      nextParams.set('pageSize', String(nextState.pageSize));
+    } else {
+      nextParams.delete('pageSize');
+    }
+
+    const nextParamString = nextParams.toString();
+    const nextUrl = nextParamString ? `${ALERT_INHIBIT_ROUTE_PATH}?${nextParamString}` : ALERT_INHIBIT_ROUTE_PATH;
+    const currentUrl = routeSearchParamString ? `${ALERT_INHIBIT_ROUTE_PATH}?${routeSearchParamString}` : ALERT_INHIBIT_ROUTE_PATH;
+    if (nextUrl !== currentUrl) {
+      router.replace(nextUrl, { scroll: false });
+    }
+  }, [routeSearchParamString, router]);
 
   const load = useCallback(async () => {
     const labelOptionsPromise = withTimeoutFallback(
@@ -202,13 +337,18 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
               );
               setEntityPrefillSource(prefill.source);
               setEntityPrefillWarning(prefill.warning);
-              setDraft(buildAlertInhibitFormDraft(null, { ...baseDraftPatch, ...prefill.draftPatch }));
+              const nextDraft = buildAlertInhibitFormDraft(null, { ...baseDraftPatch, ...prefill.draftPatch });
+              setDraft(nextDraft);
+              setEditorInitialFingerprint(serializeAlertInhibitDraft(nextDraft));
             } finally {
               setEditorLoading(false);
             }
           } else {
-            setDraft(buildAlertInhibitFormDraft(null, baseDraftPatch));
+            const nextDraft = buildAlertInhibitFormDraft(null, baseDraftPatch);
+            setDraft(nextDraft);
+            setEditorInitialFingerprint(serializeAlertInhibitDraft(nextDraft));
           }
+          setEditorDiscardDialogOpen(false);
           setEditorError(null);
           setEditorErrorDetail(null);
           setEditorErrorContract(null);
@@ -227,7 +367,10 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
           setEditorMessage(null);
           try {
             const detail = await loadAlertInhibitDetailFromFacade(api.alertInhibits.detail, targetId);
-            setDraft(buildAlertInhibitFormDraft(detail));
+            const nextDraft = buildAlertInhibitFormDraft(detail);
+            setDraft(nextDraft);
+            setEditorInitialFingerprint(serializeAlertInhibitDraft(nextDraft));
+            setEditorDiscardDialogOpen(false);
             setEditorOpen(true);
           } catch (error) {
             setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
@@ -241,9 +384,13 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
         async function handleSave() {
           const validationError = validateAlertInhibitForm(draft, t);
           if (validationError) {
+            const validationField = getAlertInhibitValidationField(draft);
             setEditorError(validationError);
             setEditorErrorDetail(null);
             setEditorErrorContract(null);
+            if (validationField) {
+              focusAlertInhibitEditorField(validationField);
+            }
             return;
           }
           setEditorSaving(true);
@@ -258,8 +405,10 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
             } else {
               await createAlertInhibitFromFacade(api.alertInhibits.create, draft);
             }
+            setEditorInitialFingerprint(serializeAlertInhibitDraft(draft));
             setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditorOpen(false);
+            setEditorDiscardDialogOpen(false);
             setCreatedOutsideMatchedViewNotice(!isEdit && useMatchedView);
             setRefreshTick(value => value + 1);
           } catch (error) {
@@ -271,6 +420,22 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
           }
         }
 
+        function handleCloseEditor() {
+          setEditorOpen(false);
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorDiscardDialogOpen(false);
+        }
+
+        function requestCloseEditor() {
+          if (shouldConfirmEditorDiscard) {
+            setEditorDiscardDialogOpen(true);
+            return;
+          }
+          handleCloseEditor();
+        }
+
         async function handleToggleEnabled(inhibit?: AlertInhibit) {
           const target = inhibit ?? selected;
           if (!target) return;
@@ -344,35 +509,78 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
         }
 
         function handleApplyFilter() {
-          setQuery(search);
+          const nextSearch = search.trim();
+          const nextState = { search: nextSearch, pageIndex: 0, pageSize };
+          setSearch(nextSearch);
+          setQuery(nextSearch);
           setPageIndex(0);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery(nextState);
         }
 
         function handleClearFilter() {
+          const nextState = { search: '', pageIndex: 0, pageSize };
           setSearch('');
           setQuery('');
           setPageIndex(0);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery(nextState);
         }
 
         function handlePageIndexChange(nextPageIndex: number) {
           setPageIndex(nextPageIndex);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery({ search: query, pageIndex: nextPageIndex, pageSize });
         }
 
         function handlePageSizeChange(nextPageSize: number) {
+          const nextState = { search: query, pageIndex: 0, pageSize: nextPageSize };
           setPageSize(nextPageSize);
           setPageIndex(0);
           setSelectedId(null);
           setCheckedIds([]);
+          replaceRouteQuery(nextState);
         }
 
         return (
           <>
+            {(() => {
+              const deleteTargetNames = deleteRequest?.ids
+                .map(id => data.list.content.find(item => item.id === id)?.name?.trim())
+                .filter((name): name is string => Boolean(name)) ?? [];
+              const missingDeleteTargetCount = deleteRequest
+                ? Math.max(deleteRequest.ids.length - deleteTargetNames.length, 0)
+                : 0;
+              const deleteConfirmCopy = [
+                deleteRequest?.kind === 'batch'
+                  ? t('alert.inhibit.delete.confirm.batch', { count: deleteRequest.ids.length })
+                  : t('alert.inhibit.delete.confirm.single'),
+                deleteTargetNames.length > 0
+                  ? t('alert.inhibit.delete.confirm.targets', { names: deleteTargetNames.join(', ') })
+                  : null,
+                missingDeleteTargetCount > 0
+                  ? t('alert.inhibit.delete.confirm.targets-more', { count: missingDeleteTargetCount })
+                  : null
+              ].filter(Boolean).join('\n');
+
+              return (
+                <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
+                  <HzConfirmDialog
+                    open={Boolean(deleteRequest)}
+                    title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
+                    copy={deleteConfirmCopy}
+                    confirmLabel={t('alert.inhibit.delete.confirm.action')}
+                    cancelLabel={t('common.button.cancel')}
+                    pending={deletePending}
+                    onCancel={() => setDeleteRequest(null)}
+                    onConfirm={() => void handleConfirmedDelete()}
+                  />
+                </div>
+              );
+            })()}
             <AlertInhibitSurface
               t={t}
               data={data}
@@ -404,6 +612,7 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
               onSelect={setSelectedId}
               onCheckedIdsChange={setCheckedIds}
               pageSizeOptions={[...ALERT_INHIBIT_PAGE_SIZE_OPTIONS]}
+              requestedPageSize={pageSize}
               onPageIndexChange={handlePageIndexChange}
               onPageSizeChange={handlePageSizeChange}
               onViewAllRules={() => {
@@ -425,27 +634,26 @@ export default function AlertInhibitPage({ initialRouteState }: { initialRouteSt
               onToggleEnabled={inhibit => void handleToggleEnabled(inhibit)}
               onDelete={inhibitId => void handleDelete(inhibitId)}
               onDeleteSelected={() => void handleDeleteSelected()}
-              onCloseEditor={() => setEditorOpen(false)}
+              onCloseEditor={requestCloseEditor}
               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'}>
+            <div
+              data-alert-inhibit-unsaved-cancel="hertzbeat-ui-confirm-dialog"
+              data-alert-inhibit-unsaved-cancel-state={editorDiscardDialogOpen ? 'open' : 'closed'}
+            >
               <HzConfirmDialog
-                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()}
+                open={editorDiscardDialogOpen}
+                title={t('alert.inhibit.unsaved-cancel.title')}
+                kicker={t('alert.inhibit.unsaved-cancel.kicker')}
+                copy={t('alert.inhibit.unsaved-cancel.copy')}
+                confirmLabel={t('alert.inhibit.unsaved-cancel.discard')}
+                cancelLabel={t('alert.inhibit.unsaved-cancel.keep-editing')}
+                onCancel={() => setEditorDiscardDialogOpen(false)}
+                onConfirm={handleCloseEditor}
               />
             </div>
           </>
diff --git a/web-next/app/alert/inhibit/page.test.tsx b/web-next/app/alert/inhibit/page.test.tsx
index 7c50eabd2a..d0c9bbc594 100644
--- a/web-next/app/alert/inhibit/page.test.tsx
+++ b/web-next/app/alert/inhibit/page.test.tsx
@@ -1,13 +1,20 @@
+// @vitest-environment jsdom
+
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import React from 'react';
+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';
 import type { AlertInhibitRouteState } from '../../../lib/alert-inhibit/query-state';
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
+  lastSurfaceProps: null as null | Record<string, any>,
+  currentSearchParams: '',
+  routerReplace: vi.fn(),
   renderData: {
     list: {
       totalElements: 1,
@@ -30,6 +37,16 @@ const mockState = vi.hoisted(() => ({
 
 const apiMessageGet = vi.hoisted(() => vi.fn());
 
+(globalThis as { React?: typeof React }).React = React;
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: mockState.routerReplace
+  }),
+  useSearchParams: () => new URLSearchParams(mockState.currentSearchParams)
+}));
+
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock()
@@ -56,51 +73,46 @@ vi.mock('../../../components/workbench/client-workbench', () => ({
 }));
 
 vi.mock('../../../components/pages/alert-inhibit-surface', () => ({
-  AlertInhibitSurface: ({
-    data,
-    returnContext,
-    managementContext,
-    matchedViewEnabled,
-    missingMatchedRuleCount,
-    createdOutsideMatchedViewNotice,
-    entityPrefillSource,
-    entityPrefillWarning,
-    evidenceContext,
-    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 ?? ''}
-      data-alert-inhibit-prefill-source-labels={evidenceContext?.sourceLabelsText ?? ''}
-      data-alert-inhibit-draft-source-labels={draft?.sourceLabelsText ?? ''}
-      data-alert-inhibit-draft-target-labels={draft?.targetLabelsText ?? ''}
-      data-alert-inhibit-draft-equal-labels={draft?.equalLabelsText ?? ''}
-    />
-  )
+  AlertInhibitSurface: (props: any) => {
+    const {
+      data,
+      returnContext,
+      managementContext,
+      matchedViewEnabled,
+      missingMatchedRuleCount,
+      createdOutsideMatchedViewNotice,
+      entityPrefillSource,
+      entityPrefillWarning,
+      evidenceContext,
+      draft,
+      pageSizeOptions,
+      search
+    } = props;
+    mockState.lastSurfaceProps = props;
+    return (
+      <div
+        data-alert-inhibit-surface="true"
+        data-total={data.list.totalElements}
+        data-page-size-options={pageSizeOptions?.join('|')}
+        data-requested-page-size={props.requestedPageSize}
+        data-search={search}
+        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 ?? ''}
+        data-alert-inhibit-prefill-source-labels={evidenceContext?.sourceLabelsText ?? ''}
+        data-alert-inhibit-draft-source-labels={draft?.sourceLabelsText ?? ''}
+        data-alert-inhibit-draft-target-labels={draft?.targetLabelsText ?? ''}
+        data-alert-inhibit-draft-equal-labels={draft?.equalLabelsText ?? ''}
+      />
+    );
+  }
 }));
 
 vi.mock('../../../lib/api-client', () => ({
@@ -139,8 +151,25 @@ async function renderAlertInhibitPage(initialRouteState: AlertInhibitRouteState
 }
 
 describe('alert inhibit page', () => {
+  let interactionContainer: HTMLDivElement | null = null;
+  let interactionRoot: Root | null = null;
+
+  afterEach(() => {
+    if (interactionRoot) {
+      act(() => {
+        interactionRoot?.unmount();
+      });
+    }
+    interactionRoot = null;
+    interactionContainer?.remove();
+    interactionContainer = null;
+  });
+
   beforeEach(() => {
     mockState.lastLoad = null;
+    mockState.lastSurfaceProps = null;
+    mockState.currentSearchParams = '';
+    mockState.routerReplace.mockReset();
     apiMessageGet.mockClear().mockResolvedValue(mockState.renderData.list);
   });
 
@@ -149,6 +178,7 @@ describe('alert inhibit page', () => {
 
     expect(html).toContain('data-alert-inhibit-surface="true"');
     expect(html).toContain('data-page-size-options="8|15|25"');
+    expect(html).toContain('data-search=""');
     expect(html).toContain('data-loading-copy="Loading inhibit rules"');
 
     await mockState.lastLoad?.();
@@ -269,7 +299,7 @@ describe('alert inhibit page', () => {
     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).toContain("import { useRouter, useSearchParams } from 'next/navigation';");
     expect(source).not.toContain('queryStateFromParams(searchParams)');
     expect(source).not.toContain('readSignalRouteContext(searchParams)');
     expect(source).toContain('const alertInhibitRouteState = initialRouteState ?? EMPTY_ALERT_INHIBIT_ROUTE_STATE');
@@ -282,8 +312,8 @@ describe('alert inhibit page', () => {
     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('const [pageIndex, setPageIndex] = useState(routeListState.pageIndex)');
+    expect(source).toContain('const [pageSize, setPageSize] = useState<number>(routeListState.pageSize)');
     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]');
@@ -303,6 +333,148 @@ describe('alert inhibit page', () => {
     expect(source).toContain('setPageIndex(0);');
   });
 
+  it('initializes alert inhibit list state from the route and preserves URL state during search and pagination', async () => {
+    mockState.currentSearchParams = 'search=ops&pageIndex=2&pageSize=15&signal=metrics&entityId=7';
+    const { default: AlertInhibitPage } = await import('./alert-inhibit-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<AlertInhibitPage />);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.search).toBe('ops');
+    expect(mockState.lastSurfaceProps?.requestedPageSize).toBe(15);
+    await act(async () => {
+      await mockState.lastLoad?.();
+    });
+    expect(apiMessageGet).toHaveBeenCalledWith('/alert/inhibits?pageIndex=2&pageSize=15&sort=id&order=desc&search=ops');
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSearchChange('checkout');
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onApplyFilter();
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/inhibit?search=checkout&pageSize=15&signal=metrics&entityId=7', { scroll: false });
+
+    mockState.currentSearchParams = 'search=checkout&pageSize=15&signal=metrics&entityId=7';
+    await act(async () => {
+      interactionRoot?.render(<AlertInhibitPage />);
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onPageIndexChange(3);
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/inhibit?search=checkout&pageSize=15&signal=metrics&entityId=7&pageIndex=3', { scroll: false });
+
+    mockState.currentSearchParams = 'search=checkout&pageSize=15&signal=metrics&entityId=7';
+    await act(async () => {
+      interactionRoot?.render(<AlertInhibitPage />);
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onPageSizeChange(8);
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/inhibit?search=checkout&signal=metrics&entityId=7', { scroll: false });
+
+    mockState.currentSearchParams = 'search=checkout&pageSize=15&signal=metrics&entityId=7';
+    await act(async () => {
+      interactionRoot?.render(<AlertInhibitPage />);
+      await Promise.resolve();
+    });
+    await act(async () => {
+      mockState.lastSurfaceProps?.onClearFilter();
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenLastCalledWith('/alert/inhibit?signal=metrics&entityId=7', { scroll: false });
+  });
+
+  it('clears local editor validation when canceling an empty new inhibit draft', async () => {
+    const { default: AlertInhibitPage } = await import('./alert-inhibit-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<AlertInhibitPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSave();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.editorError).toBe(createTranslatorMock()('alert.inhibit.validation.name'));
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onCloseEditor();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.editorOpen).toBe(false);
+    expect(mockState.lastSurfaceProps?.editorError).toBeNull();
+    expect(mockState.lastSurfaceProps?.editorErrorDetail).toBeNull();
+    expect(mockState.lastSurfaceProps?.editorErrorContract).toBeNull();
+  });
+
+  it('asks for confirmation before closing a dirty inhibit editor draft', async () => {
+    const { default: AlertInhibitPage } = await import('./alert-inhibit-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<AlertInhibitPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onNew();
+      await Promise.resolve();
+    });
+
+    const draft = mockState.lastSurfaceProps?.draft;
+    expect(mockState.lastSurfaceProps?.editorOpen).toBe(true);
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDraftChange({ ...draft, name: 'Unsaved inhibit draft' });
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onCloseEditor();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.editorOpen).toBe(true);
+    expect(interactionContainer.innerHTML).toContain('data-alert-inhibit-unsaved-cancel-state="open"');
+    expect(interactionContainer.innerHTML).toContain('Discard unsaved inhibit changes?');
+  });
+
+  it('focuses the first invalid inhibit editor field after local validation fails', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+
+    expect(source).toContain('function focusAlertInhibitEditorField(field: AlertInhibitValidationField)');
+    expect(source).toContain('const validationField = getAlertInhibitValidationField(draft)');
+    expect(source).toContain('focusAlertInhibitEditorField(validationField)');
+    expect(source).toContain('input[name="inhibit_name"]');
+    expect(source).toContain('[data-alert-inhibit-source-label-selector] [data-hz-label-selector-draft-row="true"] input[data-hz-label-selector-key-input="searchable-key"]');
+    expect(source).toContain('[data-alert-inhibit-target-label-selector] [data-hz-label-selector-draft-row="true"] input[data-hz-label-selector-key-input="searchable-key"]');
+    expect(source).toContain('[data-alert-inhibit-equal-label-selector] input[data-hz-tag-input-control="draft"]');
+  });
+
   it('loads Angular entity-noise-control matched inhibit rules by id', async () => {
     const initialRouteState: AlertInhibitRouteState = {
       returnContext: {
@@ -413,6 +585,16 @@ describe('alert inhibit page', () => {
     expect(source).not.toContain("t('common.delete-failed')");
   });
 
+  it('names the target and destructive action in inhibit delete confirmation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+
+    expect(source).toContain('const deleteTargetNames = deleteRequest?.ids');
+    expect(source).toContain("t('alert.inhibit.delete.confirm.targets', { names: deleteTargetNames.join(', ') })");
+    expect(source).toContain("t('alert.inhibit.delete.confirm.targets-more', { count: missingDeleteTargetCount })");
+    expect(source).toContain("confirmLabel={t('alert.inhibit.delete.confirm.action')}");
+    expect(source).not.toContain("confirmLabel={t('common.button.ok')}");
+  });
+
   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(
diff --git a/web-next/app/alert/integration/[source]/alert-integration-source-redirect.tsx b/web-next/app/alert/integration/[source]/alert-integration-source-redirect.tsx
new file mode 100644
index 0000000000..5c8b3b17b5
--- /dev/null
+++ b/web-next/app/alert/integration/[source]/alert-integration-source-redirect.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import React from 'react';
+import { useEffect } from 'react';
+
+export function AlertIntegrationSourceRedirect({ href }: { href: string }) {
+  useEffect(() => {
+    window.location.replace(href);
+  }, [href]);
+
+  return (
+    <section
+      data-alert-integration-canonical-redirect="pending"
+      className="min-h-[260px]"
+    />
+  );
+}
diff --git a/web-next/app/alert/integration/[source]/page.test.tsx b/web-next/app/alert/integration/[source]/page.test.tsx
index 6ee3908fdf..cbfb55ae97 100644
--- a/web-next/app/alert/integration/[source]/page.test.tsx
+++ b/web-next/app/alert/integration/[source]/page.test.tsx
@@ -4,7 +4,6 @@ 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(() =>
@@ -38,10 +37,6 @@ vi.mock('next/link', () => ({
   )
 }));
 
-vi.mock('next/navigation', () => ({
-  redirect
-}));
-
 vi.mock('next/headers', () => ({
   headers
 }));
@@ -151,6 +146,7 @@ describe('alert integration page', () => {
     expect(html).toContain('data-alert-integration-token-action-owner="hertzbeat-ui-button-link"');
     expect(html).toContain('data-alert-integration-source-item="webhook"');
     expect(html).toContain('data-alert-integration-source-selected="true"');
+    expect(html).toContain('aria-current="page"');
     expect(html).toContain('data-alert-integration-source-icon="webhook"');
     expect(html).toContain('data-hz-source-doc-item-owner="hertzbeat-ui-source-doc-shell"');
     expect(html).toContain('data-hz-source-doc-item-icon-owner="hertzbeat-ui-source-doc-shell"');
@@ -160,7 +156,8 @@ describe('alert integration page', () => {
     expect(html).toContain('data-alert-integration-markdown="rendered"');
     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('data-alert-integration-mermaid="structured-flow"');
+    expect(html).toContain('data-alert-integration-diagram-runtime="semantic-html"');
     expect(html).toContain('Alert integration');
     expect(html).toContain('Integration alert sources');
     expect(html).toContain('Default Webhook');
@@ -194,6 +191,7 @@ describe('alert integration page', () => {
     expect(source).toContain('loadIntegrationDoc(baseDir, selectedSource.id, locale)');
     expect(source).toContain('data-alert-integration-surface="hertzbeat-ui-source-doc"');
     expect(source).toContain('data-alert-integration-shell-owner="hertzbeat-ui-source-doc-shell"');
+    expect(source).toContain("'aria-current': item.id === selectedSource.id ? ('page' as const) : undefined");
     expect(source).not.toContain('h-[calc(100vh-242px)]');
     expect(source).toContain("'data-alert-integration-source-icon': item.id");
     expect(source).toContain('AlertIntegrationMarkdown');
@@ -217,19 +215,24 @@ describe('alert integration page', () => {
     expect(source).not.toContain("from '@/components/observability/code-pane'");
   });
 
-  it('redirects unknown source params to the canonical default source route', async () => {
+  it('uses a client canonical redirect for unknown source params so production hydration does not strand an empty shell', async () => {
     headers.mockResolvedValue(new Headers({ 'accept-language': 'zh-CN' }));
-    redirect.mockImplementation((target: string) => {
-      throw new Error(`redirect:${target}`);
-    });
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/integration/[source]/page.tsx'), 'utf8');
+    const redirectSource = readFileSync(resolve(process.cwd(), 'app/alert/integration/[source]/alert-integration-source-redirect.tsx'), 'utf8');
 
     const { default: AlertIntegrationPage } = await import('./page');
 
-    await expect(
-      AlertIntegrationPage({
+    const html = renderToStaticMarkup(
+      await AlertIntegrationPage({
         params: Promise.resolve({ source: 'unknown-provider' })
       })
-    ).rejects.toThrow('redirect:/alert/integration/webhook');
-    expect(redirect).toHaveBeenLastCalledWith('/alert/integration/webhook');
+    );
+
+    expect(html).toContain('data-alert-integration-canonical-redirect="pending"');
+    expect(source).toContain('AlertIntegrationSourceRedirect');
+    expect(source).toContain('return <AlertIntegrationSourceRedirect href={buildAlertIntegrationSourceHref(selectedSource)} />');
+    expect(source).not.toContain("from 'next/navigation'");
+    expect(redirectSource).toContain('window.location.replace(href)');
+    expect(redirectSource).toContain('data-alert-integration-canonical-redirect="pending"');
   });
 });
diff --git a/web-next/app/alert/integration/[source]/page.tsx b/web-next/app/alert/integration/[source]/page.tsx
index 2fdf0d1f78..8965f7f46c 100644
--- a/web-next/app/alert/integration/[source]/page.tsx
+++ b/web-next/app/alert/integration/[source]/page.tsx
@@ -1,7 +1,6 @@
 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 { HzSourceDocShell } from '@hertzbeat/ui/source-doc-shell';
@@ -15,12 +14,13 @@ import {
 } from '@/lib/alert-integration/view-model';
 import { normalizeLocale } from '@/lib/i18n';
 import { AlertIntegrationMarkdown } from '../../../../components/pages/alert-integration-markdown';
+import { AlertIntegrationSourceRedirect } from './alert-integration-source-redirect';
 
 export default async function AlertIntegrationPage({ params }: { params: Promise<{ source: string }> }) {
   const { source } = await params;
   const selectedSource = getIntegrationSource(source.trim());
   if (selectedSource.id !== source) {
-    redirect(buildAlertIntegrationSourceHref(selectedSource));
+    return <AlertIntegrationSourceRedirect href={buildAlertIntegrationSourceHref(selectedSource)} />;
   }
   const requestHeaders = await headers();
   const locale = normalizeLocale(requestHeaders.get('accept-language'));
@@ -41,7 +41,8 @@ export default async function AlertIntegrationPage({ params }: { params: Promise
       selected: item.id === selectedSource.id,
       itemProps: {
         'data-alert-integration-source-item': item.id,
-        'data-alert-integration-source-selected': item.id === selectedSource.id ? 'true' : undefined
+        'data-alert-integration-source-selected': item.id === selectedSource.id ? 'true' : undefined,
+        'aria-current': item.id === selectedSource.id ? ('page' as const) : undefined
       },
       iconProps: {
         'data-alert-integration-source-icon': item.id
diff --git a/web-next/app/alert/notice/alert-notice-page.tsx b/web-next/app/alert/notice/alert-notice-page.tsx
index 16a5056066..565a754c37 100644
--- a/web-next/app/alert/notice/alert-notice-page.tsx
+++ b/web-next/app/alert/notice/alert-notice-page.tsx
@@ -1,13 +1,14 @@
 'use client';
 
-import React, { useCallback, useMemo, useState } from 'react';
-import { ArrowLeft, Eye, Inbox, MoreHorizontal, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { ArrowLeft, CircleHelp, 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';
 import { AlertNoticeReceiverFields } from '../../../components/pages/alert-notice-receiver-fields';
-import { AlertNoticeRuleFields } from '../../../components/pages/alert-notice-rule-fields';
+import { AlertNoticeRuleFields, type NoticeRuleValidationIssue } from '../../../components/pages/alert-notice-rule-fields';
 import { AlertNoticeTemplateFields } from '../../../components/pages/alert-notice-template-fields';
 import { Button } from '../../../components/ui/button';
 import { SearchRow } from '../../../components/ui/search-row';
@@ -22,7 +23,7 @@ import {
 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 { buildAlertNoticeEvidenceContext, buildNoticeReceiverDraft, buildNoticeReceiverValidationIssues, buildNoticeTemplateDraft, buildNoticeTemplateValidationIssues, getAlertNoticeProductCopy, validateNoticeRuleDraft, type NoticeReceiverValidationIssue, type NoticeTemplateValidationIssue } from '../../../lib/alert-notice/view-model';
 import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptionsFromFacade, type AlertLabelOptions } from '../../../lib/alert-label-options';
 import { hzOpsCatalogVisual } from '../../../lib/hz-ops-visual';
 import { formatTime } from '../../../lib/format';
@@ -40,12 +41,49 @@ type NoticePageData = {
 type NoticeDeleteRequest = {
   kind: 'receiver' | 'rule' | 'template';
   id: number;
+  name?: string;
+};
+
+type AlertNoticeActionHelpCopy = {
+  label: string;
+  body: string;
+  impact?: string;
+};
+export type AlertNoticeRouteTab = 'receiver' | 'rule' | 'template';
+export type AlertNoticeListRouteState = {
+  selectedTab: AlertNoticeRouteTab;
+  receiverSearch: string;
+  receiverPageIndex: number;
+  receiverPageSize: number;
+  ruleSearch: string;
+  rulePageIndex: number;
+  rulePageSize: number;
+  templateSearch: string;
+  templatePresetFilter: boolean;
+  templatePageIndex: number;
+  templatePageSize: number;
 };
 
 const NOTICE_PAGE_SIZE_OPTIONS = [8, 15, 25];
 type Translator = ReturnType<typeof useI18n>['t'];
 const coldNoticeVisual = hzOpsCatalogVisual;
 const coldToolbarClass = 'flex flex-wrap items-center gap-2 border-b border-[#252b34] bg-[#0b0c0e] px-3 py-2';
+
+function resolveNoticeRuleValidationIssues(
+  validationError: string | null,
+  t: Translator
+): NoticeRuleValidationIssue[] {
+  if (!validationError) return [];
+  const validationMap: Array<{ field: NoticeRuleValidationIssue['field']; key: string }> = [
+    { field: 'name', key: 'alert.notice.rule.validation.name' },
+    { field: 'receiver', key: 'alert.notice.rule.validation.receivers' },
+    { field: 'labels', key: 'alert.notice.rule.validation.labels' },
+    { field: 'days', key: 'alert.notice.rule.validation.days' },
+    { field: 'time', key: 'alert.notice.rule.validation.period' }
+  ];
+  const matchedIssue = validationMap.find(issue => t(issue.key) === validationError);
+  return matchedIssue ? [{ field: matchedIssue.field, message: validationError }] : [];
+}
 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]';
@@ -58,12 +96,218 @@ 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 coldStickyActionHeaderClass =
+  'sticky right-0 z-10 w-[168px] border-l border-[#252b34] bg-[#101217] px-3 py-3 text-center shadow-[-12px_0_18px_rgba(0,0,0,0.28)]';
+const coldStickyActionCellClass =
+  'sticky right-0 z-10 border-l border-[#252b34] bg-[#0b0c0e] px-3 py-3 text-center shadow-[-12px_0_18px_rgba(0,0,0,0.28)]';
+const coldNoticeStatusMessageClass = 'px-3 py-2 text-sm text-[#b9c6d8]';
+const coldNoticeTestStatusMessageClass =
+  'rounded-[3px] border border-[#2f3a4a] bg-[#111722] px-3 py-2 text-[12px] font-semibold leading-5 text-[#c6d4e6]';
 const ALERT_NOTICE_SETTLED_CACHE_TTL_MS = 10_000;
+const RECEIVER_TEST_SEND_TIMEOUT_MS = 15_000;
+const NOTICE_RECEIVER_DRAFT_FINGERPRINT_FIELDS: Array<keyof NoticeReceiverDraft> = [
+  'id',
+  'name',
+  'type',
+  'email',
+  'phone',
+  'hookUrl',
+  'hookAuthType',
+  'hookAuthToken',
+  'wechatId',
+  'accessToken',
+  'tgBotToken',
+  'tgUserId',
+  'tgMessageThreadId',
+  'larkReceiveType',
+  'userId',
+  'chatId',
+  'slackWebHookUrl',
+  'corpId',
+  'agentId',
+  'appSecret',
+  'partyId',
+  'tagId',
+  'discordChannelId',
+  'discordBotToken',
+  'smnAk',
+  'smnSk',
+  'smnProjectId',
+  'smnRegion',
+  'smnTopicUrn',
+  'serverChanToken',
+  'gotifyToken',
+  'appId'
+];
+const NOTICE_RULE_DRAFT_FINGERPRINT_FIELDS: Array<keyof NoticeRuleDraft> = [
+  'id',
+  'name',
+  'receiverIdsText',
+  'templateId',
+  'enable',
+  'filterAll',
+  'labelsText',
+  'daysText',
+  'periodLimit',
+  'periodStart',
+  'periodEnd'
+];
+const NOTICE_TEMPLATE_DRAFT_FINGERPRINT_FIELDS: Array<keyof NoticeTemplateDraft> = [
+  'id',
+  'name',
+  'type',
+  'preset',
+  'content'
+];
 const EMPTY_ALERT_NOTICE_ROUTE_STATE: AlertNoticeRouteState = {
   signal: null,
   signalContext: {}
 };
 
+function serializeNoticeReceiverDraft(draft: NoticeReceiverDraft) {
+  return JSON.stringify(
+    NOTICE_RECEIVER_DRAFT_FINGERPRINT_FIELDS.map(field => [field, draft[field] == null ? '' : String(draft[field]).trim()])
+  );
+}
+
+function serializeNoticeRuleDraft(draft: NoticeRuleDraft) {
+  return JSON.stringify(
+    NOTICE_RULE_DRAFT_FINGERPRINT_FIELDS.map(field => [field, draft[field] == null ? '' : String(draft[field]).trim()])
+  );
+}
+
+function serializeNoticeTemplateDraft(draft: NoticeTemplateDraft) {
+  return JSON.stringify(
+    NOTICE_TEMPLATE_DRAFT_FINGERPRINT_FIELDS.map(field => [field, draft[field] == null ? '' : String(draft[field]).trim()])
+  );
+}
+
+async function withNoticeReceiverTestTimeout<T>(task: Promise<T>, error: Error) {
+  let timeoutId: ReturnType<typeof setTimeout> | null = null;
+  try {
+    return await Promise.race([
+      task,
+      new Promise<T>((_, reject) => {
+        timeoutId = setTimeout(() => reject(error), RECEIVER_TEST_SEND_TIMEOUT_MS);
+      })
+    ]);
+  } finally {
+    if (timeoutId) clearTimeout(timeoutId);
+  }
+}
+
+function parseAlertNoticeRouteInteger(value: string | null, fallback: number, minimum = 0) {
+  if (!value) return fallback;
+  const parsed = Number.parseInt(value, 10);
+  return Number.isFinite(parsed) && parsed >= minimum ? parsed : fallback;
+}
+
+function normalizeAlertNoticeTab(value: string | null, fallback: AlertNoticeRouteTab): AlertNoticeRouteTab {
+  return value === 'receiver' || value === 'rule' || value === 'template' ? value : fallback;
+}
+
+function writeAlertNoticeRouteParam(params: URLSearchParams, key: string, value: string) {
+  const cleanValue = value.trim();
+  if (cleanValue) {
+    params.set(key, cleanValue);
+  } else {
+    params.delete(key);
+  }
+}
+
+function writeAlertNoticeRouteIntegerParam(
+  params: URLSearchParams,
+  currentParams: URLSearchParams,
+  key: string,
+  value: number,
+  defaultValue: number
+) {
+  const nextValue = String(value);
+  if (value !== defaultValue || currentParams.get(key) === nextValue) {
+    params.set(key, nextValue);
+  } else {
+    params.delete(key);
+  }
+}
+
+export function buildAlertNoticeListRouteUrl(
+  routeSearchParamString: string,
+  defaultSelectedTab: AlertNoticeRouteTab,
+  nextState: AlertNoticeListRouteState
+) {
+  const currentParams = new URLSearchParams(routeSearchParamString);
+  const nextParams = new URLSearchParams(routeSearchParamString);
+  if (nextState.selectedTab !== defaultSelectedTab || currentParams.get('tab') === nextState.selectedTab) {
+    nextParams.set('tab', nextState.selectedTab);
+  } else {
+    nextParams.delete('tab');
+  }
+  writeAlertNoticeRouteParam(nextParams, 'receiverSearch', nextState.receiverSearch);
+  writeAlertNoticeRouteParam(nextParams, 'ruleSearch', nextState.ruleSearch);
+  writeAlertNoticeRouteParam(nextParams, 'templateSearch', nextState.templateSearch);
+
+  writeAlertNoticeRouteIntegerParam(nextParams, currentParams, 'receiverPageIndex', nextState.receiverPageIndex, 0);
+  writeAlertNoticeRouteIntegerParam(nextParams, currentParams, 'receiverPageSize', nextState.receiverPageSize, 8);
+  writeAlertNoticeRouteIntegerParam(nextParams, currentParams, 'rulePageIndex', nextState.rulePageIndex, 0);
+  writeAlertNoticeRouteIntegerParam(nextParams, currentParams, 'rulePageSize', nextState.rulePageSize, 8);
+  writeAlertNoticeRouteIntegerParam(nextParams, currentParams, 'templatePageIndex', nextState.templatePageIndex, 0);
+  writeAlertNoticeRouteIntegerParam(nextParams, currentParams, 'templatePageSize', nextState.templatePageSize, 8);
+  if (!nextState.templatePresetFilter || currentParams.get('templatePreset') === 'true') nextParams.set('templatePreset', String(nextState.templatePresetFilter));
+  else nextParams.delete('templatePreset');
+
+  const nextParamString = nextParams.toString();
+  return nextParamString ? `/alert/notice?${nextParamString}` : '/alert/notice';
+}
+
+function alertNoticeActionHelp(t: Translator, id: string): AlertNoticeActionHelpCopy {
+  const impactKey = `alert.notice.action.${id}.impact`;
+  const impact = t(impactKey);
+  return {
+    label: t('alert.notice.action.help-aria', { action: t(`alert.notice.action.${id}.label`) }),
+    body: t(`alert.notice.action.${id}.help`),
+    impact: impact === impactKey ? undefined : impact
+  };
+}
+
+function AlertNoticeActionHelp({
+  id,
+  label,
+  body,
+  impact
+}: AlertNoticeActionHelpCopy & {
+  id: string;
+}) {
+  return (
+    <span
+      data-alert-notice-action-help={id}
+      className="group/help relative inline-flex h-5 w-5 shrink-0 items-center justify-center"
+    >
+      <button
+        type="button"
+        aria-label={label}
+        data-alert-notice-action-help-trigger="hertzbeat-ui-action-help"
+        data-alert-notice-action-help-style="icon-after-action"
+        data-alert-notice-action-help-visual="circle-help-icon"
+        className="inline-flex h-5 w-5 items-center justify-center rounded-none border-0 bg-transparent p-0 text-[#d8e4ff] transition hover:text-[#f5f7fb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4e74f8]"
+        onClick={event => {
+          event.preventDefault();
+          event.stopPropagation();
+        }}
+      >
+        <CircleHelp size={13} strokeWidth={2} aria-hidden="true" data-alert-notice-action-help-icon="lucide-circle-help" />
+      </button>
+      <span
+        role="tooltip"
+        data-alert-notice-action-help-tooltip={id}
+        className="pointer-events-none absolute left-0 top-6 z-30 hidden w-[300px] rounded-[3px] border border-[#2b3039] bg-[#101217] p-3 text-left shadow-[0_16px_40px_rgba(0,0,0,0.42)] group-hover/help:block group-focus-within/help:block"
+      >
+        <span className="block text-[11px] leading-4 text-[#dbe4f0]">{body}</span>
+        {impact ? <span className="mt-2 block border-t border-[#2b3039] pt-2 text-[11px] leading-4 text-[#98a2b3]">{impact}</span> : null}
+      </span>
+    </span>
+  );
+}
+
 function isApiMessageBusinessError(error: unknown) {
   return typeof error === 'object' && error !== null && typeof (error as { code?: unknown }).code === 'number';
 }
@@ -248,19 +492,24 @@ function NoticePagination({
 function NoticeTableEmptyRow({
   t,
   colSpan,
-  prefix
+  prefix,
+  action
 }: {
   t: Translator;
   colSpan: number;
   prefix: 'receiver' | 'rule' | 'template';
+  action?: {
+    label: string;
+    onClick: () => void;
+  };
 }) {
   return (
     <tr data-alert-notice-empty-row="true" data-alert-notice-receiver-empty-state={prefix === 'receiver' ? 'hertzbeat-ui-empty-state' : undefined} className="bg-[#0b0c0e]">
       <td
         colSpan={colSpan}
-        className="h-[360px] px-3 pt-[54px] text-center align-top text-[#8f99ab]"
+        className="h-[360px] px-3 pt-[54px] text-left align-top text-[#8f99ab]"
       >
-        <div className="inline-flex flex-col items-center gap-2.5">
+        <div className="flex w-[min(560px,70vw)] min-w-[220px] flex-col items-center gap-2.5">
           <span
             data-alert-notice-receiver-empty-icon={prefix === 'receiver' ? 'hertzbeat-ui-empty-icon' : undefined}
             className="inline-flex h-10 w-12 items-center justify-center rounded-[4px] border border-[#2b3039] bg-[#101217] text-[#7e8494]"
@@ -268,6 +517,21 @@ function NoticeTableEmptyRow({
             <Inbox className="h-7 w-7" aria-hidden="true" />
           </span>
           <div className="text-[14px]">{t('common.no-data')}</div>
+          {action ? (
+            <Button
+              type="button"
+              size="sm"
+              variant="default"
+              data-alert-notice-receiver-empty-action={prefix === 'receiver' ? 'new' : undefined}
+              data-alert-notice-rule-empty-action={prefix === 'rule' ? 'new' : undefined}
+              data-alert-notice-template-empty-action={prefix === 'template' ? 'new' : undefined}
+              className={coldPrimaryCommandButtonClass}
+              onClick={action.onClick}
+            >
+              <Plus className="h-4 w-4" aria-hidden="true" />
+              {action.label}
+            </Button>
+          ) : null}
         </div>
       </td>
     </tr>
@@ -279,12 +543,14 @@ function NoticeTableSwitch({
   field,
   label,
   pending,
+  commandAction,
   onChange
 }: {
   checked: boolean;
   field: 'filter-all' | 'enable';
   label: string;
   pending?: boolean;
+  commandAction?: string;
   onChange: (checked: boolean) => void;
 }) {
   return (
@@ -296,6 +562,7 @@ function NoticeTableSwitch({
       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-alert-notice-command-action={commandAction}
       data-state={checked ? 'checked' : 'unchecked'}
       disabled={pending}
       onClick={() => onChange(!checked)}
@@ -311,6 +578,9 @@ function NoticeTableSwitch({
 
 export default function AlertNoticePage({ initialRouteState }: { initialRouteState?: AlertNoticeRouteState } = {}) {
   const { t, locale } = useI18n();
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const routeSearchParamString = searchParams.toString();
   const alertNoticeRouteState = initialRouteState ?? EMPTY_ALERT_NOTICE_ROUTE_STATE;
   const { signal, signalContext } = alertNoticeRouteState;
   const noticeEvidenceContext = useMemo(
@@ -319,37 +589,62 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
   );
   const [editingReceiver, setEditingReceiver] = useState(false);
   const [receiverDraft, setReceiverDraft] = useState<NoticeReceiverDraft>(() => buildNoticeReceiverDraft(null));
+  const [receiverInitialFingerprint, setReceiverInitialFingerprint] = useState(() => serializeNoticeReceiverDraft(buildNoticeReceiverDraft(null)));
+  const [receiverDiscardDialogOpen, setReceiverDiscardDialogOpen] = useState(false);
   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 defaultSelectedTab: AlertNoticeRouteTab = noticeEvidenceContext ? 'rule' : 'receiver';
+  const routeListState = useMemo<AlertNoticeListRouteState>(() => {
+    const routeParams = new URLSearchParams(routeSearchParamString);
+    return {
+      selectedTab: normalizeAlertNoticeTab(routeParams.get('tab'), defaultSelectedTab),
+      receiverSearch: routeParams.get('receiverSearch') ?? '',
+      receiverPageIndex: parseAlertNoticeRouteInteger(routeParams.get('receiverPageIndex'), 0),
+      receiverPageSize: parseAlertNoticeRouteInteger(routeParams.get('receiverPageSize'), 8, 1),
+      ruleSearch: routeParams.get('ruleSearch') ?? '',
+      rulePageIndex: parseAlertNoticeRouteInteger(routeParams.get('rulePageIndex'), 0),
+      rulePageSize: parseAlertNoticeRouteInteger(routeParams.get('rulePageSize'), 8, 1),
+      templateSearch: routeParams.get('templateSearch') ?? '',
+      templatePresetFilter: routeParams.get('templatePreset') === 'false' ? false : true,
+      templatePageIndex: parseAlertNoticeRouteInteger(routeParams.get('templatePageIndex'), 0),
+      templatePageSize: parseAlertNoticeRouteInteger(routeParams.get('templatePageSize'), 8, 1)
+    };
+  }, [defaultSelectedTab, routeSearchParamString]);
+  const [receiverSearchDraft, setReceiverSearchDraft] = useState(routeListState.receiverSearch);
+  const [receiverSearch, setReceiverSearch] = useState(routeListState.receiverSearch);
+  const [receiverPageIndex, setReceiverPageIndex] = useState(routeListState.receiverPageIndex);
+  const [receiverPageSize, setReceiverPageSize] = useState(routeListState.receiverPageSize);
   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 [receiverValidationIssues, setReceiverValidationIssues] = useState<NoticeReceiverValidationIssue[]>([]);
   const [editingTemplate, setEditingTemplate] = useState(false);
   const [templateDraft, setTemplateDraft] = useState<NoticeTemplateDraft>(() => buildNoticeTemplateDraft(null));
+  const [templateInitialFingerprint, setTemplateInitialFingerprint] = useState(() => serializeNoticeTemplateDraft(buildNoticeTemplateDraft(null)));
+  const [templateDiscardDialogOpen, setTemplateDiscardDialogOpen] = useState(false);
   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 [templateErrorDetail, setTemplateErrorDetail] = 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 [templateValidationIssues, setTemplateValidationIssues] = useState<NoticeTemplateValidationIssue[]>([]);
+  const [templateSearchDraft, setTemplateSearchDraft] = useState(routeListState.templateSearch);
+  const [templateSearch, setTemplateSearch] = useState(routeListState.templateSearch);
+  const [templatePresetFilter, setTemplatePresetFilter] = useState(routeListState.templatePresetFilter);
+  const [templatePageIndex, setTemplatePageIndex] = useState(routeListState.templatePageIndex);
+  const [templatePageSize, setTemplatePageSize] = useState(routeListState.templatePageSize);
+  const [selectedTab, setSelectedTab] = useState<AlertNoticeConsoleTabKey>(() => routeListState.selectedTab);
   const [editingRule, setEditingRule] = useState(false);
   const [ruleDraft, setRuleDraft] = useState<NoticeRuleDraft>(() => buildNoticeRuleDraft(null));
+  const [ruleInitialFingerprint, setRuleInitialFingerprint] = useState(() => serializeNoticeRuleDraft(buildNoticeRuleDraft(null)));
+  const [ruleDiscardDialogOpen, setRuleDiscardDialogOpen] = useState(false);
   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 [ruleSearchDraft, setRuleSearchDraft] = useState(routeListState.ruleSearch);
+  const [ruleSearch, setRuleSearch] = useState(routeListState.ruleSearch);
+  const [rulePageIndex, setRulePageIndex] = useState(routeListState.rulePageIndex);
+  const [rulePageSize, setRulePageSize] = useState(routeListState.rulePageSize);
   const [savingRule, setSavingRule] = useState(false);
   const [ruleMessage, setRuleMessage] = useState<string | null>(null);
   const [ruleError, setRuleError] = useState<string | null>(null);
@@ -364,6 +659,14 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
     () => buildNoticeTemplateListUrl({ search: templateSearch, preset: templatePresetFilter, pageIndex: templatePageIndex, pageSize: templatePageSize }),
     [templatePageIndex, templatePageSize, templatePresetFilter, templateSearch]
   );
+  const receiverDraftFingerprint = useMemo(() => serializeNoticeReceiverDraft(receiverDraft), [receiverDraft]);
+  const shouldConfirmReceiverDiscard = Boolean(editingReceiver && receiverDraftFingerprint !== receiverInitialFingerprint && !savingReceiver);
+  const ruleDraftFingerprint = useMemo(() => serializeNoticeRuleDraft(ruleDraft), [ruleDraft]);
+  const shouldConfirmRuleDiscard = Boolean(editingRule && ruleDraftFingerprint !== ruleInitialFingerprint && !savingRule);
+  const templateDraftFingerprint = useMemo(() => serializeNoticeTemplateDraft(templateDraft), [templateDraft]);
+  const shouldConfirmTemplateDiscard = Boolean(
+    editingTemplate && !templateReadOnly && templateDraftFingerprint !== templateInitialFingerprint && !savingTemplate
+  );
   const alertNoticeLoadQuery = useMemo(
     () => ({
       receivers: {
@@ -389,6 +692,44 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
     () => ['alert-notice', alertNoticeReceiverListUrl, alertNoticeRuleListUrl, alertNoticeTemplateListUrl, refreshTick].join('|'),
     [alertNoticeReceiverListUrl, alertNoticeRuleListUrl, alertNoticeTemplateListUrl, refreshTick]
   );
+  const currentListRouteState = useMemo<AlertNoticeListRouteState>(() => ({
+    selectedTab: selectedTab as AlertNoticeRouteTab,
+    receiverSearch,
+    receiverPageIndex,
+    receiverPageSize,
+    ruleSearch,
+    rulePageIndex,
+    rulePageSize,
+    templateSearch,
+    templatePresetFilter,
+    templatePageIndex,
+    templatePageSize
+  }), [receiverPageIndex, receiverPageSize, receiverSearch, rulePageIndex, rulePageSize, ruleSearch, selectedTab, templatePageIndex, templatePageSize, templatePresetFilter, templateSearch]);
+
+  useEffect(() => {
+    setSelectedTab(routeListState.selectedTab);
+    setReceiverSearchDraft(routeListState.receiverSearch);
+    setReceiverSearch(routeListState.receiverSearch);
+    setReceiverPageIndex(routeListState.receiverPageIndex);
+    setReceiverPageSize(routeListState.receiverPageSize);
+    setRuleSearchDraft(routeListState.ruleSearch);
+    setRuleSearch(routeListState.ruleSearch);
+    setRulePageIndex(routeListState.rulePageIndex);
+    setRulePageSize(routeListState.rulePageSize);
+    setTemplateSearchDraft(routeListState.templateSearch);
+    setTemplateSearch(routeListState.templateSearch);
+    setTemplatePresetFilter(routeListState.templatePresetFilter);
+    setTemplatePageIndex(routeListState.templatePageIndex);
+    setTemplatePageSize(routeListState.templatePageSize);
+  }, [routeListState]);
+
+  const replaceListRouteState = useCallback((nextState: AlertNoticeListRouteState) => {
+    const nextUrl = buildAlertNoticeListRouteUrl(routeSearchParamString, defaultSelectedTab, nextState);
+    const currentUrl = routeSearchParamString ? `/alert/notice?${routeSearchParamString}` : '/alert/notice';
+    if (nextUrl !== currentUrl) {
+      router.replace(nextUrl, { scroll: false });
+    }
+  }, [defaultSelectedTab, routeSearchParamString, router]);
 
   const load = useCallback(async (): Promise<NoticePageData> => {
     void refreshTick;
@@ -451,12 +792,66 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
         const ruleTotal = data.rules.totalElements ?? data.rules.content.length;
         const templateTotal = data.templates.totalElements ?? data.templates.content.length;
 
+        function closeReceiverEditor() {
+          setEditingReceiver(false);
+          setReceiverDiscardDialogOpen(false);
+          setReceiverMessage(null);
+          setReceiverError(null);
+          setReceiverErrorDetail(null);
+          setReceiverValidationIssues([]);
+        }
+
+        function requestCloseReceiverEditor() {
+          if (shouldConfirmReceiverDiscard) {
+            setReceiverDiscardDialogOpen(true);
+            return;
+          }
+          closeReceiverEditor();
+        }
+
+        function closeTemplateEditor() {
+          setEditingTemplate(false);
+          setTemplateReadOnly(false);
+          setTemplateDiscardDialogOpen(false);
+          setTemplateMessage(null);
+          setTemplateError(null);
+          setTemplateErrorDetail(null);
+          setTemplateValidationIssues([]);
+        }
+
+        function requestCloseTemplateEditor() {
+          if (shouldConfirmTemplateDiscard) {
+            setTemplateDiscardDialogOpen(true);
+            return;
+          }
+          closeTemplateEditor();
+        }
+
+        function closeRuleEditor() {
+          setEditingRule(false);
+          setRuleDiscardDialogOpen(false);
+          setRuleMessage(null);
+          setRuleError(null);
+          setRuleErrorDetail(null);
+        }
+
+        function requestCloseRuleEditor() {
+          if (shouldConfirmRuleDiscard) {
+            setRuleDiscardDialogOpen(true);
+            return;
+          }
+          closeRuleEditor();
+        }
+
         async function handleNewReceiver() {
           setSelectedTab('receiver');
-          setReceiverDraft(buildNoticeReceiverDraft(null));
+          const nextDraft = buildNoticeReceiverDraft(null);
+          setReceiverDraft(nextDraft);
+          setReceiverInitialFingerprint(serializeNoticeReceiverDraft(nextDraft));
           setReceiverMessage(null);
           setReceiverError(null);
           setReceiverErrorDetail(null);
+          setReceiverValidationIssues([]);
           setEditingReceiver(true);
         }
 
@@ -466,10 +861,13 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             setSelectedTab('receiver');
             setSelectedReceiverId(receiver.id);
             const detail = await api.alertNotice.receivers.detail(receiver.id);
-            setReceiverDraft(buildNoticeReceiverDraft(detail));
+            const nextDraft = buildNoticeReceiverDraft(detail);
+            setReceiverDraft(nextDraft);
+            setReceiverInitialFingerprint(serializeNoticeReceiverDraft(nextDraft));
             setReceiverMessage(null);
             setReceiverError(null);
             setReceiverErrorDetail(null);
+            setReceiverValidationIssues([]);
             setEditingReceiver(true);
           } catch (error) {
             setReceiverError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
@@ -477,15 +875,31 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
           }
         }
 
+        function focusReceiverValidationField(field: NoticeReceiverValidationIssue['field']) {
+          window.setTimeout(() => {
+            document.querySelector<HTMLElement>(`[data-testid="notice-receiver-field-${String(field)}"]`)?.focus();
+          }, 0);
+        }
+
+        function handleReceiverDraftChange(nextDraft: React.SetStateAction<NoticeReceiverDraft>) {
+          setReceiverDraft(nextDraft);
+          setReceiverValidationIssues([]);
+          setReceiverError(null);
+          setReceiverErrorDetail(null);
+        }
+
         async function handleSaveReceiver() {
           const isEdit = Boolean(receiverDraft.id);
-          const validationError = validateNoticeReceiverDraft(receiverDraft, t);
-          if (validationError) {
+          const validationIssues = buildNoticeReceiverValidationIssues(receiverDraft, t);
+          if (validationIssues.length > 0) {
+            setReceiverValidationIssues(validationIssues);
             setReceiverMessage(null);
-            setReceiverError(validationError);
+            setReceiverError(validationIssues.map(issue => issue.message).join(', '));
             setReceiverErrorDetail(null);
+            focusReceiverValidationField(validationIssues[0].field);
             return;
           }
+          setReceiverValidationIssues([]);
           setSavingReceiver(true);
           setReceiverMessage(null);
           setReceiverError(null);
@@ -496,6 +910,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             } else {
               await api.alertNotice.receivers.create(receiverDraft);
             }
+            setReceiverInitialFingerprint(serializeNoticeReceiverDraft(receiverDraft));
             setReceiverMessage([t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'), t('alert.notice.receiver.next')].join(' '));
             setEditingReceiver(false);
             setRefreshTick(value => value + 1);
@@ -512,11 +927,11 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
 
         async function handleDeleteReceiver() {
           if (!selectedReceiver?.id) return;
-          await handleDeleteReceiverById(selectedReceiver.id);
+          await handleDeleteReceiverById(selectedReceiver.id, selectedReceiver.name);
         }
 
-        async function handleDeleteReceiverById(receiverId: number) {
-          setDeleteRequest({ kind: 'receiver', id: receiverId });
+        async function handleDeleteReceiverById(receiverId: number, receiverName?: string | null) {
+          setDeleteRequest({ kind: 'receiver', id: receiverId, name: receiverName?.trim() || undefined });
         }
 
         async function handleConfirmedDelete() {
@@ -575,14 +990,17 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
           setReceiverError(null);
           setReceiverErrorDetail(null);
           try {
-            await api.alertNotice.receivers.sendTest(receiverDraft);
+            await withNoticeReceiverTestTimeout(
+              api.alertNotice.receivers.sendTest(receiverDraft),
+              new Error(t('alert.notice.send-test.timeout.detail'))
+            );
             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);
+            setReceiverError(t('alert.notice.send-test.notify.failed'));
+            setReceiverErrorDetail(error instanceof Error ? error.message : null);
           } finally {
             setTestingReceiver(false);
           }
@@ -590,11 +1008,14 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
 
         async function handleNewTemplate() {
           setSelectedTab('template');
-          setTemplateDraft(buildNoticeTemplateDraft(null));
+          const nextDraft = buildNoticeTemplateDraft(null);
+          setTemplateDraft(nextDraft);
+          setTemplateInitialFingerprint(serializeNoticeTemplateDraft(nextDraft));
           setTemplateReadOnly(false);
           setTemplateMessage(null);
           setTemplateError(null);
           setTemplateErrorDetail(null);
+          setTemplateValidationIssues([]);
           setEditingTemplate(true);
         }
 
@@ -605,10 +1026,13 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             setSelectedTemplateId(template.id);
             setTemplateReadOnly(false);
             const detail = await api.alertNotice.templates.detail(template.id);
-            setTemplateDraft(buildNoticeTemplateDraft(detail));
+            const nextDraft = buildNoticeTemplateDraft(detail);
+            setTemplateDraft(nextDraft);
+            setTemplateInitialFingerprint(serializeNoticeTemplateDraft(nextDraft));
             setTemplateMessage(null);
             setTemplateError(null);
             setTemplateErrorDetail(null);
+            setTemplateValidationIssues([]);
             setEditingTemplate(true);
           } catch (error) {
             setTemplateError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
@@ -623,31 +1047,51 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
           setSelectedTemplateId(templateId);
           setTemplateReadOnly(true);
           setTemplateDraft(rowDraft);
+          setTemplateInitialFingerprint(serializeNoticeTemplateDraft(rowDraft));
           setTemplateMessage(null);
           setTemplateError(null);
           setTemplateErrorDetail(null);
+          setTemplateValidationIssues([]);
           setEditingTemplate(true);
 
           if (templateId == null) return;
 
           try {
             const detail = await api.alertNotice.templates.detail(templateId);
-            setTemplateDraft(buildNoticeTemplateDraft(detail));
+            const nextDraft = buildNoticeTemplateDraft(detail);
+            setTemplateDraft(nextDraft);
+            setTemplateInitialFingerprint(serializeNoticeTemplateDraft(nextDraft));
           } catch (error) {
             setTemplateError(null);
             setTemplateErrorDetail(null);
           }
         }
 
+        function focusTemplateValidationField(field: NoticeTemplateValidationIssue['field']) {
+          window.setTimeout(() => {
+            document.querySelector<HTMLElement>(`[data-testid="notice-template-field-${String(field)}"]`)?.focus();
+          }, 0);
+        }
+
+        function handleTemplateDraftChange(nextDraft: React.SetStateAction<NoticeTemplateDraft>) {
+          setTemplateDraft(nextDraft);
+          setTemplateValidationIssues([]);
+          setTemplateError(null);
+          setTemplateErrorDetail(null);
+        }
+
         async function handleSaveTemplate() {
           const isEdit = Boolean(templateDraft.id);
-          const validationError = validateNoticeTemplateDraft(templateDraft, t);
-          if (validationError) {
+          const validationIssues = buildNoticeTemplateValidationIssues(templateDraft, t);
+          if (validationIssues.length > 0) {
+            setTemplateValidationIssues(validationIssues);
             setTemplateMessage(null);
-            setTemplateError(validationError);
+            setTemplateError(validationIssues.map(issue => issue.message).join(', '));
             setTemplateErrorDetail(null);
+            focusTemplateValidationField(validationIssues[0].field);
             return;
           }
+          setTemplateValidationIssues([]);
           setSavingTemplate(true);
           setTemplateMessage(null);
           setTemplateError(null);
@@ -658,8 +1102,23 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             } else {
               await api.alertNotice.templates.create(templateDraft);
             }
+            setTemplateInitialFingerprint(serializeNoticeTemplateDraft(templateDraft));
             setTemplateMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditingTemplate(false);
+            if (!isEdit) {
+              const nextTemplateSearch = templateDraft.name.trim();
+              setTemplateSearchDraft(nextTemplateSearch);
+              setTemplateSearch(nextTemplateSearch);
+              setTemplatePresetFilter(false);
+              setTemplatePageIndex(0);
+              replaceListRouteState({
+                ...currentListRouteState,
+                selectedTab: 'template',
+                templateSearch: nextTemplateSearch,
+                templatePresetFilter: false,
+                templatePageIndex: 0
+              });
+            }
             setRefreshTick(value => value + 1);
           } catch (error) {
             setTemplateError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
@@ -671,27 +1130,40 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
 
         async function handleDeleteTemplate() {
           if (!selectedTemplate?.id || selectedTemplate.preset) return;
-          await handleDeleteTemplateById(selectedTemplate.id);
+          await handleDeleteTemplateById(selectedTemplate.id, selectedTemplate.name);
         }
 
-        async function handleDeleteTemplateById(templateId: number) {
-          setDeleteRequest({ kind: 'template', id: templateId });
+        async function handleDeleteTemplateById(templateId: number, templateName?: string | null) {
+          setDeleteRequest({ kind: 'template', id: templateId, name: templateName?.trim() || undefined });
         }
 
         function commitTemplateSearch() {
-          setTemplateSearch(templateSearchDraft);
+          const nextSearch = templateSearchDraft.trim();
+          setTemplateSearch(nextSearch);
           setTemplatePageIndex(0);
+          replaceListRouteState({
+            ...currentListRouteState,
+            templateSearch: nextSearch,
+            templatePageIndex: 0
+          });
         }
 
         function resetTemplateSearch() {
           setTemplateSearchDraft('');
           setTemplateSearch('');
           setTemplatePageIndex(0);
+          replaceListRouteState({
+            ...currentListRouteState,
+            templateSearch: '',
+            templatePageIndex: 0
+          });
         }
 
         async function handleNewRule() {
           setSelectedTab('rule');
-          setRuleDraft(buildNoticeRuleDraft(null, noticeEvidenceContext?.ruleDraftPatch));
+          const nextDraft = buildNoticeRuleDraft(null, noticeEvidenceContext?.ruleDraftPatch);
+          setRuleDraft(nextDraft);
+          setRuleInitialFingerprint(serializeNoticeRuleDraft(nextDraft));
           setRuleMessage(null);
           setRuleError(null);
           setRuleErrorDetail(null);
@@ -705,7 +1177,9 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             setSelectedTab('rule');
             setSelectedRuleId(ruleId);
             const detail = await api.alertNotice.rules.detail(ruleId);
-            setRuleDraft(buildNoticeRuleDraft(detail));
+            const nextDraft = buildNoticeRuleDraft(detail);
+            setRuleDraft(nextDraft);
+            setRuleInitialFingerprint(serializeNoticeRuleDraft(nextDraft));
             setRuleMessage(null);
             setRuleError(null);
             setRuleErrorDetail(null);
@@ -716,6 +1190,13 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
           }
         }
 
+        function handleRuleDraftChange(nextDraft: React.SetStateAction<NoticeRuleDraft>) {
+          setRuleDraft(nextDraft);
+          setRuleMessage(null);
+          setRuleError(null);
+          setRuleErrorDetail(null);
+        }
+
         async function handleSaveRule() {
           const isEdit = Boolean(ruleDraft.id);
           const validationError = validateNoticeRuleDraft(ruleDraft, t);
@@ -736,6 +1217,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             } else {
               await api.alertNotice.rules.create(ruleDraft, displayNames);
             }
+            setRuleInitialFingerprint(serializeNoticeRuleDraft(ruleDraft));
             setRuleMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditingRule(false);
             setRefreshTick(value => value + 1);
@@ -749,11 +1231,11 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
 
         async function handleDeleteRule() {
           if (!selectedRule?.id) return;
-          await handleDeleteRuleById(selectedRule.id);
+          await handleDeleteRuleById(selectedRule.id, selectedRule.name);
         }
 
-        async function handleDeleteRuleById(ruleId: number) {
-          setDeleteRequest({ kind: 'rule', id: ruleId });
+        async function handleDeleteRuleById(ruleId: number, ruleName?: string | null) {
+          setDeleteRequest({ kind: 'rule', id: ruleId, name: ruleName?.trim() || undefined });
         }
 
         async function handleToggleRuleSwitch(rule: NoticeRule, field: 'filterAll' | 'enable', checked: boolean) {
@@ -782,25 +1264,47 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
         }
 
         function commitReceiverSearch() {
-          setReceiverSearch(receiverSearchDraft.trim());
+          const nextSearch = receiverSearchDraft.trim();
+          setReceiverSearch(nextSearch);
           setReceiverPageIndex(0);
+          replaceListRouteState({
+            ...currentListRouteState,
+            receiverSearch: nextSearch,
+            receiverPageIndex: 0
+          });
         }
 
         function resetReceiverSearch() {
           setReceiverSearchDraft('');
           setReceiverSearch('');
           setReceiverPageIndex(0);
+          replaceListRouteState({
+            ...currentListRouteState,
+            receiverSearch: '',
+            receiverPageIndex: 0
+          });
         }
 
         function commitRuleSearch() {
-          setRuleSearch(ruleSearchDraft.trim());
+          const nextSearch = ruleSearchDraft.trim();
+          setRuleSearch(nextSearch);
           setRulePageIndex(0);
+          replaceListRouteState({
+            ...currentListRouteState,
+            ruleSearch: nextSearch,
+            rulePageIndex: 0
+          });
         }
 
         function resetRuleSearch() {
           setRuleSearchDraft('');
           setRuleSearch('');
           setRulePageIndex(0);
+          replaceListRouteState({
+            ...currentListRouteState,
+            ruleSearch: '',
+            rulePageIndex: 0
+          });
         }
 
         const receiverPanel = (
@@ -811,35 +1315,46 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
               className={coldToolbarClass}
             >
               <div className="flex flex-wrap items-center gap-2">
-                <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>
-                <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="hertzbeat-ui-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>
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    data-alert-notice-receiver-sync="angular-load-table"
+                    data-alert-notice-receiver-sync-owner="route-refresh-contract"
+                    data-alert-notice-command-action="receiver-refresh"
+                    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>
+                  <AlertNoticeActionHelp id="receiver-refresh" {...alertNoticeActionHelp(t, 'receiver-refresh')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <Button data-testid="notice-receiver-new" data-alert-notice-command-action="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>
+                  <AlertNoticeActionHelp id="receiver-new" {...alertNoticeActionHelp(t, 'receiver-new')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    data-alert-notice-receiver-bulk-menu="hertzbeat-ui-more"
+                    data-alert-notice-command-action="receiver-delete"
+                    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>
+                  <AlertNoticeActionHelp id="receiver-delete" {...alertNoticeActionHelp(t, 'receiver-delete')} />
+                </span>
               </div>
                 <SearchRow
                   data-alert-notice-receiver-search="shared-compact"
@@ -856,14 +1371,14 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
               />
             </div>
             <AlertSurfaceTableShell data-alert-notice-receiver-table-shell="hertzbeat-ui-dense-table" className={coldTableShellClass}>
-              <AlertSurfaceTable className="min-w-[1240px] text-center">
+              <AlertSurfaceTable data-alert-notice-receiver-table-layout="viewport-contained-visible-actions" className="w-full table-fixed 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>
+                    <th className="w-[26%] px-3 py-3 text-center">{t('alert.notice.receiver.people')}</th>
+                    <th className="w-[112px] px-3 py-3 text-center">{t('alert.notice.receiver.type')}</th>
+                    <th className="w-[26%] px-3 py-3 text-center">{t('alert.notice.receiver.setting')}</th>
+                    <th className="w-[148px] px-3 py-3 text-center">{t('common.edit-time')}</th>
+                    <th className="w-[132px] px-3 py-3 text-center">{t('common.edit')}</th>
                   </tr>
                 </AlertSurfaceTableHead>
                 <tbody>
@@ -874,43 +1389,61 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                       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={`truncate px-3 py-3 text-center font-medium ${coldTextPrimaryClass}`} title={receiver.name || t('alert.notice.receiver.people')}>
+                        {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, emptyValue)}</AlertSurfaceValuePill>
                         </div>
                       </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="truncate px-3 py-3 text-center" title={getReceiverSetting(receiver, emptyValue)}>{getReceiverSetting(receiver, emptyValue)}</td>
+                      <td className="whitespace-nowrap 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={() => void handleEditReceiver(receiver)}
-                            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 className="flex justify-center gap-1.5">
+                          <span className="inline-flex items-center gap-1">
+                            <Button
+                              data-alert-notice-receiver-edit-detail="angular-detail-fetch"
+                              data-alert-notice-receiver-edit-detail-owner="route-detail-fetch-contract"
+                              data-alert-notice-command-action="receiver-row-edit"
+                              className={coldIconButtonClass}
+                              size="icon"
+                              variant="default"
+                              onClick={() => void handleEditReceiver(receiver)}
+                              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>
+                            <AlertNoticeActionHelp id="receiver-row-edit" {...alertNoticeActionHelp(t, 'receiver-row-edit')} />
+                          </span>
+                          <span className="inline-flex items-center gap-1">
+                            <Button
+                              data-alert-notice-command-action="receiver-row-delete"
+                              className={coldIconButtonClass}
+                              size="icon"
+                              variant="default"
+                              onClick={() => void handleDeleteReceiverById(receiver.id, receiver.name)}
+                              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>
+                            <AlertNoticeActionHelp id="receiver-row-delete" {...alertNoticeActionHelp(t, 'receiver-row-delete')} />
+                          </span>
                         </div>
                       </td>
                     </tr>
                   )) : (
-                    <NoticeTableEmptyRow t={t} colSpan={5} prefix="receiver" />
+                    <NoticeTableEmptyRow
+                      t={t}
+                      colSpan={5}
+                      prefix="receiver"
+                      action={{
+                        label: t('alert.notice.receiver.new'),
+                        onClick: () => void handleNewReceiver()
+                      }}
+                    />
                   )}
                 </tbody>
               </AlertSurfaceTable>
@@ -923,14 +1456,25 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 totalElements={data.receivers.totalElements || 0}
                 visibleCount={data.receivers.content.length}
                 testIdPrefix="notice-receiver"
-                onPageIndexChange={setReceiverPageIndex}
+                onPageIndexChange={nextPageIndex => {
+                  setReceiverPageIndex(nextPageIndex);
+                  replaceListRouteState({
+                    ...currentListRouteState,
+                    receiverPageIndex: nextPageIndex
+                  });
+                }}
                 onPageSizeChange={nextPageSize => {
                   setReceiverPageSize(nextPageSize);
                   setReceiverPageIndex(0);
+                  replaceListRouteState({
+                    ...currentListRouteState,
+                    receiverPageIndex: 0,
+                    receiverPageSize: nextPageSize
+                  });
                 }}
               />
             ) : null}
-            {!editingReceiver && receiverMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{receiverMessage}</div> : null}
+            {!editingReceiver && receiverMessage ? <div role="status" className={coldNoticeStatusMessageClass}>{receiverMessage}</div> : null}
             {!editingReceiver && receiverError ? (
               <div
                 role="alert"
@@ -961,26 +1505,36 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
               className={coldToolbarClass}
             >
               <div className="flex flex-wrap items-center gap-2">
-                <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>
-                <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>
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    data-alert-notice-rule-sync="angular-load-table"
+                    data-alert-notice-rule-sync-owner="route-refresh-contract"
+                    data-alert-notice-command-action="rule-refresh"
+                    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>
+                  <AlertNoticeActionHelp id="rule-refresh" {...alertNoticeActionHelp(t, 'rule-refresh')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <Button data-alert-notice-command-action="rule-new" className={coldPrimaryCommandButtonClass} variant="default" size="sm" onClick={() => void handleNewRule()}>
+                    <Plus className="h-4 w-4" aria-hidden="true" />
+                    {t('alert.notice.rule.new')}
+                  </Button>
+                  <AlertNoticeActionHelp id="rule-new" {...alertNoticeActionHelp(t, 'rule-new')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <Button data-alert-notice-command-action="rule-delete" 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>
+                  <AlertNoticeActionHelp id="rule-delete" {...alertNoticeActionHelp(t, 'rule-delete')} />
+                </span>
               </div>
                 <SearchRow
                   data-alert-notice-rule-search="shared-compact"
@@ -997,7 +1551,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
               />
             </div>
             <AlertSurfaceTableShell data-alert-notice-rule-table-shell="hertzbeat-ui-dense-table" className={coldTableShellClass}>
-              <AlertSurfaceTable className="min-w-[1240px] text-center">
+              <AlertSurfaceTable data-alert-notice-rule-table-actions="sticky-visible-actions" 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>
@@ -1006,7 +1560,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                     <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>
+                    <th className={coldStickyActionHeaderClass}>{t('common.edit')}</th>
                   </tr>
                 </AlertSurfaceTableHead>
                 <tbody>
@@ -1021,47 +1575,70 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                       <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"
-                          checked={Boolean(rule.filterAll)}
-                          pending={ruleSwitchPending === `${rule.id}:filterAll`}
-                          label={t('alert.notice.rule.all')}
-                          onChange={checked => void handleToggleRuleSwitch(rule, 'filterAll', checked)}
-                        />
+                        <span className="inline-flex items-center gap-1">
+                          <NoticeTableSwitch
+                            field="filter-all"
+                            commandAction="rule-row-filter-all"
+                            checked={Boolean(rule.filterAll)}
+                            pending={ruleSwitchPending === `${rule.id}:filterAll`}
+                            label={t('alert.notice.rule.all')}
+                            onChange={checked => void handleToggleRuleSwitch(rule, 'filterAll', checked)}
+                          />
+                          <AlertNoticeActionHelp id="rule-row-filter-all" {...alertNoticeActionHelp(t, 'rule-row-filter-all')} />
+                        </span>
                       </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)}
-                        />
+                        <span className="inline-flex items-center gap-1">
+                          <NoticeTableSwitch
+                            field="enable"
+                            commandAction="rule-row-enable"
+                            checked={Boolean(rule.enable)}
+                            pending={ruleSwitchPending === `${rule.id}:enable`}
+                            label={t('common.enable')}
+                            onChange={checked => void handleToggleRuleSwitch(rule, 'enable', checked)}
+                          />
+                          <AlertNoticeActionHelp id="rule-row-enable" {...alertNoticeActionHelp(t, 'rule-row-enable')} />
+                        </span>
                       </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"
-                            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')}
-                          >
-                            <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>
+                      <td className={coldStickyActionCellClass} onClick={event => event.stopPropagation()}>
+                        <div className="flex justify-center gap-1.5">
+                          <span className="inline-flex items-center gap-1">
+                            <Button
+                              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"
+                              data-alert-notice-command-action="rule-row-edit"
+                              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>
+                            <AlertNoticeActionHelp id="rule-row-edit" {...alertNoticeActionHelp(t, 'rule-row-edit')} />
+                          </span>
+                          <span className="inline-flex items-center gap-1">
+                            <Button data-alert-notice-command-action="rule-row-delete" className={coldIconButtonClass} size="icon" variant="default" onClick={() => void handleDeleteRuleById(rule.id, rule.name)} 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>
+                            <AlertNoticeActionHelp id="rule-row-delete" {...alertNoticeActionHelp(t, 'rule-row-delete')} />
+                          </span>
                         </div>
                       </td>
                     </tr>
                   )) : (
-                    <NoticeTableEmptyRow t={t} colSpan={7} prefix="rule" />
+                    <NoticeTableEmptyRow
+                      t={t}
+                      colSpan={7}
+                      prefix="rule"
+                      action={{
+                        label: t('alert.notice.rule.new'),
+                        onClick: () => void handleNewRule()
+                      }}
+                    />
                   )}
                 </tbody>
               </AlertSurfaceTable>
@@ -1074,14 +1651,25 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 totalElements={data.rules.totalElements || 0}
                 visibleCount={data.rules.content.length}
                 testIdPrefix="notice-rule"
-                onPageIndexChange={setRulePageIndex}
+                onPageIndexChange={nextPageIndex => {
+                  setRulePageIndex(nextPageIndex);
+                  replaceListRouteState({
+                    ...currentListRouteState,
+                    rulePageIndex: nextPageIndex
+                  });
+                }}
                 onPageSizeChange={nextPageSize => {
                   setRulePageSize(nextPageSize);
                   setRulePageIndex(0);
+                  replaceListRouteState({
+                    ...currentListRouteState,
+                    rulePageIndex: 0,
+                    rulePageSize: nextPageSize
+                  });
                 }}
               />
             ) : null}
-            {ruleMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{ruleMessage}</div> : null}
+            {ruleMessage ? <div role="status" className={coldNoticeStatusMessageClass}>{ruleMessage}</div> : null}
             {!editingRule && ruleError ? (
               <div role="alert" className="px-3 py-2 text-sm text-rose-300">{ruleError}</div>
             ) : null}
@@ -1102,36 +1690,53 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
               className={coldToolbarClass}
             >
               <div className="flex flex-wrap items-center gap-2">
-                <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>
-                <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>
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    data-alert-notice-template-sync="angular-load-table"
+                    data-alert-notice-template-sync-owner="route-refresh-contract"
+                    data-alert-notice-command-action="template-refresh"
+                    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>
+                  <AlertNoticeActionHelp id="template-refresh" {...alertNoticeActionHelp(t, 'template-refresh')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <Button data-alert-notice-command-action="template-new" className={coldPrimaryCommandButtonClass} variant="default" size="sm" onClick={() => void handleNewTemplate()}>
+                    <Plus className="h-4 w-4" aria-hidden="true" />
+                    {t('alert.notice.template.new')}
+                  </Button>
+                  <AlertNoticeActionHelp id="template-new" {...alertNoticeActionHelp(t, 'template-new')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <Button data-alert-notice-command-action="template-delete" 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>
+                  <AlertNoticeActionHelp id="template-delete" {...alertNoticeActionHelp(t, 'template-delete')} />
+                </span>
               </div>
               <div className="flex min-w-0 flex-wrap items-center gap-2">
                 <Select
                   data-alert-notice-template-preset-filter="hertzbeat-ui-select"
                   data-alert-notice-template-preset-query="server-param"
+                  data-alert-notice-command-action="template-filter-preset"
                   aria-label={t('monitor.status')}
                   value={templatePresetFilter ? 'true' : 'false'}
                   onChange={event => {
-                    setTemplatePresetFilter(event.target.value === 'true');
+                    const nextPresetFilter = event.target.value === 'true';
+                    setTemplatePresetFilter(nextPresetFilter);
                     setTemplatePageIndex(0);
+                    replaceListRouteState({
+                      ...currentListRouteState,
+                      templatePresetFilter: nextPresetFilter,
+                      templatePageIndex: 0
+                    });
                   }}
                   containerClassName="w-[132px]"
                   className={coldSelectClass}
@@ -1155,14 +1760,14 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
               </div>
             </div>
             <AlertSurfaceTableShell data-alert-notice-template-table-shell="hertzbeat-ui-dense-table" className={coldTableShellClass}>
-              <AlertSurfaceTable className="min-w-[1240px] text-center">
+              <AlertSurfaceTable data-alert-notice-template-table-actions="sticky-visible-actions" 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>
+                    <th className={coldStickyActionHeaderClass}>{t('common.edit')}</th>
                   </tr>
                 </AlertSurfaceTableHead>
                 <tbody>
@@ -1185,45 +1790,64 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                         </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">
+                      <td className={coldStickyActionCellClass} onClick={event => event.stopPropagation()}>
+                        <div className="flex justify-center gap-1.5">
                           {template.preset ? (
-                            <Button
-                              data-alert-notice-template-view-trigger="hertzbeat-ui-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>
-                          ) : (
-                            <>
+                            <span className="inline-flex items-center gap-1">
                               <Button
-                                data-alert-notice-template-edit-detail="angular-detail-fetch"
-                                data-alert-notice-template-edit-detail-owner="route-detail-fetch-contract"
+                                data-alert-notice-template-view-trigger="hertzbeat-ui-modal-viewer-trigger"
+                                data-alert-notice-command-action="template-row-view"
                                 className={coldIconButtonClass}
                                 size="icon"
                                 variant="default"
-                                onClick={() => void handleEditTemplate(template)}
-                                title={t('alert.notice.template.edit')}
+                                onClick={() => void handleViewTemplate(template)}
+                                title={t('alert.notice.template.show')}
                               >
-                                <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>
+                                <Eye className="h-4 w-4" aria-hidden="true" />
+                                <span className="sr-only">{t('alert.notice.template.show')}</span>
                               </Button>
+                              <AlertNoticeActionHelp id="template-row-view" {...alertNoticeActionHelp(t, 'template-row-view')} />
+                            </span>
+                          ) : (
+                            <>
+                              <span className="inline-flex items-center gap-1">
+                                <Button
+                                  data-alert-notice-template-edit-detail="angular-detail-fetch"
+                                  data-alert-notice-template-edit-detail-owner="route-detail-fetch-contract"
+                                  data-alert-notice-command-action="template-row-edit"
+                                  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>
+                                <AlertNoticeActionHelp id="template-row-edit" {...alertNoticeActionHelp(t, 'template-row-edit')} />
+                              </span>
+                              <span className="inline-flex items-center gap-1">
+                                <Button data-alert-notice-command-action="template-row-delete" className={coldIconButtonClass} size="icon" variant="default" onClick={() => void handleDeleteTemplateById(template.id, template.name)} 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>
+                                <AlertNoticeActionHelp id="template-row-delete" {...alertNoticeActionHelp(t, 'template-row-delete')} />
+                              </span>
                             </>
                           )}
                         </div>
                       </td>
                     </tr>
                   )) : (
-                    <NoticeTableEmptyRow t={t} colSpan={5} prefix="template" />
+                    <NoticeTableEmptyRow
+                      t={t}
+                      colSpan={5}
+                      prefix="template"
+                      action={{
+                        label: t('alert.notice.template.new'),
+                        onClick: () => void handleNewTemplate()
+                      }}
+                    />
                   )}
                 </tbody>
               </AlertSurfaceTable>
@@ -1236,14 +1860,25 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 totalElements={templateTotalElements}
                 visibleCount={templateVisibleRows.length}
                 testIdPrefix="notice-template"
-                onPageIndexChange={setTemplatePageIndex}
+                onPageIndexChange={nextPageIndex => {
+                  setTemplatePageIndex(nextPageIndex);
+                  replaceListRouteState({
+                    ...currentListRouteState,
+                    templatePageIndex: nextPageIndex
+                  });
+                }}
                 onPageSizeChange={nextPageSize => {
                   setTemplatePageSize(nextPageSize);
                   setTemplatePageIndex(0);
+                  replaceListRouteState({
+                    ...currentListRouteState,
+                    templatePageIndex: 0,
+                    templatePageSize: nextPageSize
+                  });
                 }}
               />
             ) : null}
-            {templateMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{templateMessage}</div> : null}
+            {templateMessage ? <div role="status" className={coldNoticeStatusMessageClass}>{templateMessage}</div> : null}
             {!editingTemplate && templateError ? (
               <div role="alert" className="px-3 py-2 text-sm text-rose-300">{templateError}</div>
             ) : null}
@@ -1255,30 +1890,57 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             open={editingReceiver}
             title={receiverDraft.id ? t('alert.notice.receiver.edit') : t('alert.notice.receiver.new')}
             kicker={t('menu.alert.dispatch')}
-            onClose={() => setEditingReceiver(false)}
+            onClose={requestCloseReceiverEditor}
             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"
-                  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}>
-                  {savingReceiver ? t('common.saving') : t('common.save')}
-                </Button>
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    data-testid="notice-receiver-cancel"
+                    data-alert-notice-receiver-unsaved-cancel-trigger={shouldConfirmReceiverDiscard ? 'dirty' : 'clean'}
+                    data-alert-notice-command-action="receiver-cancel"
+                    className={coldButtonClassName}
+                    size="sm"
+                    variant="default"
+                    onClick={requestCloseReceiverEditor}
+                  >
+                    {t('common.cancel')}
+                  </Button>
+                  <AlertNoticeActionHelp id="receiver-cancel" {...alertNoticeActionHelp(t, 'receiver-cancel')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <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"
+                    data-alert-notice-command-action="receiver-test"
+                    aria-busy={testingReceiver}
+                    className={coldButtonClassName}
+                    size="sm"
+                    variant="default"
+                    onClick={() => void handleTestSend()}
+                    disabled={testingReceiver || savingReceiver}
+                  >
+                    {t('alert.notice.send-test')}
+                  </Button>
+                  <AlertNoticeActionHelp id="receiver-test" {...alertNoticeActionHelp(t, 'receiver-test')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    data-testid="notice-receiver-save"
+                    data-alert-notice-receiver-save-blocked-by-test={testingReceiver ? 'true' : undefined}
+                    data-alert-notice-command-action="receiver-save"
+                    className={coldPrimaryButtonClassName}
+                    size="sm"
+                    variant="default"
+                    onClick={() => void handleSaveReceiver()}
+                    disabled={savingReceiver || testingReceiver}
+                  >
+                    {savingReceiver ? t('common.saving') : t('common.save')}
+                  </Button>
+                  <AlertNoticeActionHelp id="receiver-save" {...alertNoticeActionHelp(t, 'receiver-save')} />
+                </span>
               </div>
             }
           >
@@ -1301,6 +1963,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                   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}
+                  data-alert-notice-receiver-validation-count={receiverValidationIssues.length > 0 ? String(receiverValidationIssues.length) : undefined}
                   className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
                 >
                   <span>{receiverError}</span>
@@ -1311,7 +1974,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 <div
                   role="status"
                   data-alert-notice-receiver-test-feedback="hertzbeat-ui-test-feedback"
-                  className="rounded-[3px] border border-[#24563d] bg-[#0d1a14] px-3 py-2 text-[12px] font-semibold leading-5 text-[#9be8bd]"
+                  className={coldNoticeTestStatusMessageClass}
                 >
                   {receiverMessage}
                 </div>
@@ -1322,6 +1985,10 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                   data-alert-notice-receiver-test-preview-owner="signal-alert-handoff"
                   data-alert-notice-receiver-test-preview-signal={noticeEvidenceContext.signal}
                   data-alert-notice-receiver-test-preview-labels="provided-labels"
+                  data-alert-notice-receiver-test-preview-labels-total={noticeEvidenceContext.receiverTestPreview.labelsTotal}
+                  data-alert-notice-receiver-test-preview-labels-rendered={noticeEvidenceContext.receiverTestPreview.labelsRendered}
+                  data-alert-notice-receiver-test-preview-labels-limit={noticeEvidenceContext.receiverTestPreview.labelsLimit}
+                  data-alert-notice-receiver-test-preview-labels-overflow={noticeEvidenceContext.receiverTestPreview.labelsOverflow}
                   className="rounded-[3px] border border-[#26303d] bg-[#080a0e] px-3 py-2"
                 >
                   <p className="text-[12px] font-semibold text-[#eef2f7]">{noticeEvidenceContext.receiverTestPreview.title}</p>
@@ -1330,7 +1997,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                     data-alert-notice-receiver-test-preview-labels-text="signal-route"
                     className="mt-2 block whitespace-pre-wrap break-words font-mono text-[11px] leading-5 text-[#aab4c3]"
                   >
-                    {noticeEvidenceContext.receiverTestPreview.labelsText}
+                    {noticeEvidenceContext.receiverTestPreview.labelsPreviewText}
                   </code>
                   <div
                     data-alert-notice-receiver-test-preview-payload="sample-alert"
@@ -1368,7 +2035,8 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 t={t}
                 draft={receiverDraft}
                 productCopy={productCopy}
-                onDraftChange={setReceiverDraft}
+                onDraftChange={handleReceiverDraftChange}
+                validationIssues={receiverValidationIssues}
               />
             </div>
           </OverlayDialog>
@@ -1379,13 +2047,14 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             open={editingRule}
             title={ruleDraft.id ? t('alert.notice.rule.edit') : t('alert.notice.rule.new')}
             kicker={t('menu.alert.dispatch')}
-            onClose={() => setEditingRule(false)}
+            onClose={requestCloseRuleEditor}
             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"
+                    data-alert-notice-command-action="rule-return-to-evidence"
                     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"
                   >
@@ -1393,22 +2062,36 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                     {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"
-                  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>
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    className={coldButtonClassName}
+                    size="sm"
+                    variant="default"
+                    data-alert-notice-rule-unsaved-cancel-trigger={shouldConfirmRuleDiscard ? 'dirty' : 'clean'}
+                    data-alert-notice-command-action="rule-cancel"
+                    onClick={requestCloseRuleEditor}
+                  >
+                    {t('common.cancel')}
+                  </Button>
+                  <AlertNoticeActionHelp id="rule-cancel" {...alertNoticeActionHelp(t, 'rule-cancel')} />
+                </span>
+                <span className="inline-flex items-center gap-1">
+                  <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'}
+                    data-alert-notice-command-action="rule-save"
+                    aria-busy={savingRule}
+                    onClick={() => void handleSaveRule()}
+                    disabled={savingRule}
+                  >
+                    {savingRule ? t('common.saving') : t('common.save')}
+                  </Button>
+                  <AlertNoticeActionHelp id="rule-save" {...alertNoticeActionHelp(t, 'rule-save')} />
+                </span>
               </div>
             }
           >
@@ -1448,7 +2131,8 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 labelOptions={labelOptions}
                 sourceLabelsText={noticeEvidenceContext?.labelsText}
                 sourceSignal={noticeEvidenceContext?.signal}
-                onDraftChange={setRuleDraft}
+                validationIssues={resolveNoticeRuleValidationIssues(ruleErrorDetail ? null : ruleError, t)}
+                onDraftChange={handleRuleDraftChange}
               />
             </div>
           </OverlayDialog>
@@ -1459,30 +2143,35 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             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);
-            }}
+            onClose={requestCloseTemplateEditor}
             maxWidthClassName="max-w-4xl"
             footer={
               <div className="flex flex-wrap justify-end gap-2">
-                <Button
-                  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);
-                  }}
-                >
-                  {templateReadOnly ? t('common.button.return') : t('common.cancel')}
-                </Button>
-                {!templateReadOnly ? (
-                  <Button className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveTemplate()} disabled={savingTemplate}>
-                    {savingTemplate ? t('common.saving') : t('common.save')}
+                <span className="inline-flex items-center gap-1">
+                  <Button
+                    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}
+                    data-alert-notice-template-unsaved-cancel-trigger={!templateReadOnly ? shouldConfirmTemplateDiscard ? 'dirty' : 'clean' : undefined}
+                    data-alert-notice-command-action={templateReadOnly ? 'template-return' : 'template-cancel'}
+                    onClick={requestCloseTemplateEditor}
+                  >
+                    {templateReadOnly ? t('common.button.return') : t('common.cancel')}
                   </Button>
+                  <AlertNoticeActionHelp
+                    id={templateReadOnly ? 'template-return' : 'template-cancel'}
+                    {...alertNoticeActionHelp(t, templateReadOnly ? 'template-return' : 'template-cancel')}
+                  />
+                </span>
+                {!templateReadOnly ? (
+                  <span className="inline-flex items-center gap-1">
+                    <Button data-alert-notice-command-action="template-save" className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveTemplate()} disabled={savingTemplate}>
+                      {savingTemplate ? t('common.saving') : t('common.save')}
+                    </Button>
+                    <AlertNoticeActionHelp id="template-save" {...alertNoticeActionHelp(t, 'template-save')} />
+                  </span>
                 ) : null}
               </div>
             }
@@ -1511,26 +2200,48 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                   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}
+                  data-alert-notice-template-validation-count={templateValidationIssues.length > 0 ? String(templateValidationIssues.length) : 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} />
+                <AlertNoticeTemplateFields
+                  t={t}
+                  draft={templateDraft}
+                  readOnly={templateReadOnly}
+                  onDraftChange={handleTemplateDraftChange}
+                  validationIssues={templateValidationIssues}
+                />
               </div>
             )}
           </OverlayDialog>
         );
+        const deleteConfirmCopy = [
+          deleteRequest?.kind === 'receiver'
+            ? t('alert.notice.delete.confirm.receiver')
+            : deleteRequest?.kind === 'template'
+              ? t('alert.notice.delete.confirm.template')
+              : t('alert.notice.delete.confirm.rule'),
+          deleteRequest?.name ? t('alert.notice.delete.confirm.target', { name: deleteRequest.name }) : null
+        ].filter(Boolean).join(' ');
+        const deleteConfirmActionLabel =
+          deleteRequest?.kind === 'receiver'
+            ? t('alert.notice.delete.confirm.receiver-action')
+            : deleteRequest?.kind === 'template'
+              ? t('alert.notice.delete.confirm.template-action')
+              : t('alert.notice.delete.confirm.rule-action');
 
         return (
           <>
             <div
               data-alert-notice-surface="otlp-hertzbeat-ui-notice-console"
               data-alert-notice-style-baseline={coldNoticeVisual.canvasName}
+              data-alert-notice-page-overflow="route-contained-horizontal"
               data-alert-notice-edit-load-feedback="angular-edit-fail"
               data-alert-notice-edit-load-feedback-owner="route-action-feedback-contract"
-              className={coldNoticeVisual.canvas.root}
+              className={`${coldNoticeVisual.canvas.root} overflow-x-hidden`}
               style={coldNoticeVisual.canvas.backgroundStyle}
             >
               <section className={coldNoticeVisual.layout.pageSection}>
@@ -1538,8 +2249,9 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                   <div className="mb-5">
                     <section
                       data-alert-notice-header="hertzbeat-ui-compact-header"
+                      data-alert-notice-header-nesting-contract="flat-page-introduction"
                       data-alert-notice-admin-layout="full-width-admin-list"
-                      className={coldNoticeVisual.panel.hero}
+                      className="p-0"
                     >
                       <div className="flex flex-wrap items-start justify-between gap-5">
                         <div className="min-w-[260px] flex-1">
@@ -1547,7 +2259,11 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                           <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}>
+                          <div
+                            data-alert-notice-command-bar="standard-equal-buttons"
+                            data-alert-notice-command-bar-mobile="two-column-wrap"
+                            className="mt-6 grid grid-cols-2 items-center gap-2 sm:flex sm:flex-wrap"
+                          >
                             <Button size="sm" className={coldCommandButtonClass} variant="default" onClick={() => setRefreshTick(value => value + 1)}>
                               {t('common.refresh')}
                             </Button>
@@ -1583,7 +2299,13 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                   <AlertNoticeConsoleShell
                     t={t}
                     selectedTab={selectedTab}
-                    onSelectTab={setSelectedTab}
+                    onSelectTab={nextTab => {
+                      setSelectedTab(nextTab);
+                      replaceListRouteState({
+                        ...currentListRouteState,
+                        selectedTab: nextTab
+                      });
+                    }}
                     receiverContent={receiverPanel}
                     ruleContent={rulePanel}
                     templateContent={templatePanel}
@@ -1591,9 +2313,10 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                   {noticeEvidenceContext ? (
                     <section
                       data-alert-notice-evidence-context="signal-route"
+                      data-alert-notice-evidence-layering="flat-context-band"
                       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)]"
+                      className="mt-5 border-y border-[#27303c] bg-[#0b0f15]/80 px-3 py-3"
                     >
                       <div className="flex flex-wrap items-start justify-between gap-3">
                         <div className="min-w-0">
@@ -1633,6 +2356,102 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
             {receiverEditorDialog}
             {ruleEditorDialog}
             {templateEditorDialog}
+            <div
+              data-alert-notice-template-unsaved-cancel="hertzbeat-ui-confirm-dialog"
+              data-alert-notice-template-unsaved-cancel-state={templateDiscardDialogOpen ? 'open' : 'closed'}
+            >
+              <HzConfirmDialog
+                open={templateDiscardDialogOpen}
+                tone="warning"
+                title={t('alert.notice.template.unsaved-cancel.title')}
+                kicker={t('alert.notice.template.unsaved-cancel.kicker')}
+                cancelLabel={t('alert.notice.template.unsaved-cancel.keep-editing')}
+                confirmLabel={t('alert.notice.template.unsaved-cancel.discard')}
+                onClose={() => setTemplateDiscardDialogOpen(false)}
+                onConfirm={closeTemplateEditor}
+                data-alert-notice-template-unsaved-cancel-dialog="hertzbeat-ui-confirm-dialog"
+                cancelButtonProps={
+                  {
+                    type: 'button',
+                    'data-alert-notice-template-unsaved-cancel-keep-editing': 'true'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['cancelButtonProps']
+                }
+                confirmButtonProps={
+                  {
+                    type: 'button',
+                    'data-alert-notice-template-unsaved-cancel-confirm': 'true'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['confirmButtonProps']
+                }
+              >
+                <p data-alert-notice-template-unsaved-cancel-copy="true">
+                  {t('alert.notice.template.unsaved-cancel.copy')}
+                </p>
+              </HzConfirmDialog>
+            </div>
+            <div
+              data-alert-notice-rule-unsaved-cancel="hertzbeat-ui-confirm-dialog"
+              data-alert-notice-rule-unsaved-cancel-state={ruleDiscardDialogOpen ? 'open' : 'closed'}
+            >
+              <HzConfirmDialog
+                open={ruleDiscardDialogOpen}
+                tone="warning"
+                title={t('alert.notice.rule.unsaved-cancel.title')}
+                kicker={t('alert.notice.rule.unsaved-cancel.kicker')}
+                cancelLabel={t('alert.notice.rule.unsaved-cancel.keep-editing')}
+                confirmLabel={t('alert.notice.rule.unsaved-cancel.discard')}
+                onClose={() => setRuleDiscardDialogOpen(false)}
+                onConfirm={closeRuleEditor}
+                data-alert-notice-rule-unsaved-cancel-dialog="hertzbeat-ui-confirm-dialog"
+                cancelButtonProps={
+                  {
+                    type: 'button',
+                    'data-alert-notice-rule-unsaved-cancel-keep-editing': 'true'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['cancelButtonProps']
+                }
+                confirmButtonProps={
+                  {
+                    type: 'button',
+                    'data-alert-notice-rule-unsaved-cancel-confirm': 'true'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['confirmButtonProps']
+                }
+              >
+                <p data-alert-notice-rule-unsaved-cancel-copy="true">
+                  {t('alert.notice.rule.unsaved-cancel.copy')}
+                </p>
+              </HzConfirmDialog>
+            </div>
+            <div
+              data-alert-notice-receiver-unsaved-cancel="hertzbeat-ui-confirm-dialog"
+              data-alert-notice-receiver-unsaved-cancel-state={receiverDiscardDialogOpen ? 'open' : 'closed'}
+            >
+              <HzConfirmDialog
+                open={receiverDiscardDialogOpen}
+                tone="warning"
+                title={t('alert.notice.receiver.unsaved-cancel.title')}
+                kicker={t('alert.notice.receiver.unsaved-cancel.kicker')}
+                cancelLabel={t('alert.notice.receiver.unsaved-cancel.keep-editing')}
+                confirmLabel={t('alert.notice.receiver.unsaved-cancel.discard')}
+                onClose={() => setReceiverDiscardDialogOpen(false)}
+                onConfirm={closeReceiverEditor}
+                data-alert-notice-receiver-unsaved-cancel-dialog="hertzbeat-ui-confirm-dialog"
+                cancelButtonProps={
+                  {
+                    type: 'button',
+                    'data-alert-notice-receiver-unsaved-cancel-keep-editing': 'true'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['cancelButtonProps']
+                }
+                confirmButtonProps={
+                  {
+                    type: 'button',
+                    'data-alert-notice-receiver-unsaved-cancel-confirm': 'true'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['confirmButtonProps']
+                }
+              >
+                <p data-alert-notice-receiver-unsaved-cancel-copy="true">
+                  {t('alert.notice.receiver.unsaved-cancel.copy')}
+                </p>
+              </HzConfirmDialog>
+            </div>
             <div
               data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}
               data-alert-notice-delete-confirm="angular-modal-confirm"
@@ -1647,7 +2466,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 tone="critical"
                 kicker={t('common.confirm.operation')}
                 title={t('common.confirm.delete')}
-                confirmLabel={t('common.button.ok')}
+                confirmLabel={deleteConfirmActionLabel}
                 cancelLabel={t('common.button.cancel')}
                 confirmDisabled={deletePending}
                 onClose={deletePending ? undefined : () => setDeleteRequest(null)}
@@ -1666,11 +2485,7 @@ export default function AlertNoticePage({ initialRouteState }: { initialRouteSta
                 }
               >
                 <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')}
+                  {deleteConfirmCopy}
                 </p>
               </HzConfirmDialog>
             </div>
diff --git a/web-next/app/alert/notice/page.test.tsx b/web-next/app/alert/notice/page.test.tsx
index eb00cc9971..6f82607b67 100644
--- a/web-next/app/alert/notice/page.test.tsx
+++ b/web-next/app/alert/notice/page.test.tsx
@@ -9,6 +9,8 @@ import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
+  currentSearchParams: '',
+  routerReplace: vi.fn(),
   renderData: {
     receivers: {
       content: [
@@ -118,6 +120,13 @@ vi.mock('next/link', () => ({
   )
 }));
 
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: mockState.routerReplace
+  }),
+  useSearchParams: () => new URLSearchParams(mockState.currentSearchParams)
+}));
+
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
@@ -346,12 +355,15 @@ async function renderAlertNoticePage(initialRouteState: AlertNoticeRouteState =
 describe('alert notice page', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
+    mockState.currentSearchParams = '';
+    mockState.routerReplace.mockReset();
     mockLoadAlertNoticeData.mockClear().mockResolvedValue(mockState.renderData);
   });
 
   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 receiverPanelSource = source.slice(source.indexOf('const receiverPanel = ('), source.indexOf('const rulePanel = ('));
     const html = await renderAlertNoticePage();
 
     expect(html).toContain(t('alert.notice.title'));
@@ -363,8 +375,12 @@ describe('alert notice page', () => {
     expect(html).toContain('data-tab="template"');
     expect(html).toContain('data-alert-notice-surface="otlp-hertzbeat-ui-notice-console"');
     expect(html).toContain('data-alert-notice-style-baseline="hertzbeat-ui-matte"');
+    expect(html).toContain('data-alert-notice-page-overflow="route-contained-horizontal"');
+    expect(html).toContain('overflow-x-hidden');
     expect(html).toContain('data-alert-notice-header="hertzbeat-ui-compact-header"');
+    expect(html).toContain('data-alert-notice-header-nesting-contract="flat-page-introduction"');
     expect(html).toContain('data-alert-notice-admin-layout="full-width-admin-list"');
+    expect(html).toContain('class="p-0"');
     expect(html).toContain('data-alert-notice-inline-metrics="hertzbeat-ui-inline-counts"');
     expect(html).toContain('data-alert-notice-command-bar="standard-equal-buttons"');
     expect(html).toContain('data-alert-notice-workbench-panel="cold-tabbed-table-panel"');
@@ -384,6 +400,10 @@ describe('alert notice page', () => {
     expect(html).not.toContain('data-hz-search-input-shell');
     expect(html).toContain('data-hz-search-action="submit"');
     expect(html).toContain('data-alert-notice-receiver-table-shell="hertzbeat-ui-dense-table"');
+    expect(html).toContain('data-alert-notice-receiver-table-layout="viewport-contained-visible-actions"');
+    expect(html).toContain('class="min-w-full border-collapse text-sm text-[var(--ops-text-secondary)] w-full table-fixed text-center"');
+    expect(receiverPanelSource).toContain('data-alert-notice-receiver-table-layout="viewport-contained-visible-actions"');
+    expect(receiverPanelSource).not.toContain('className="min-w-[1240px] text-center"');
     expect(html).toContain('data-alert-notice-pagination="hertzbeat-ui-dense-pagination"');
     expect(html).toContain('data-alert-notice-pagination-owner="hertzbeat-ui-pagination-bar"');
     expect(html).toContain('data-hz-ui="pagination-bar"');
@@ -418,11 +438,15 @@ describe('alert notice page', () => {
 
     expect(source).toContain('hzOpsCatalogVisual');
     expect(source).toContain('data-alert-notice-style-baseline={coldNoticeVisual.canvasName}');
-    expect(source).toContain('className={coldNoticeVisual.canvas.root}');
+    expect(source).toContain('data-alert-notice-page-overflow="route-contained-horizontal"');
+    expect(source).toContain('className={`${coldNoticeVisual.canvas.root} overflow-x-hidden`}');
     expect(source).toContain('style={coldNoticeVisual.canvas.backgroundStyle}');
     expect(source).toContain('<section className={coldNoticeVisual.layout.pageSection}>');
-    expect(source).toContain('className={coldNoticeVisual.panel.hero}');
-    expect(source).toContain('className={coldNoticeVisual.button.row}');
+    expect(source).toContain('data-alert-notice-header-nesting-contract="flat-page-introduction"');
+    expect(source).toContain('className="p-0"');
+    expect(source).not.toContain('className={coldNoticeVisual.panel.hero}');
+    expect(source).toContain('data-alert-notice-command-bar-mobile="two-column-wrap"');
+    expect(source).toContain('className="mt-6 grid grid-cols-2 items-center gap-2 sm:flex sm:flex-wrap"');
     expect(source).toContain('coldButtonClassName');
     expect(source).toContain('coldPrimaryButtonClassName');
     expect(source).toContain('coldCommandButtonClass');
@@ -433,6 +457,17 @@ describe('alert notice page', () => {
     expect(source).toContain("from '../../../lib/alert-label-options'");
     expect(source).toContain('loadAlertLabelOptionsFromFacade(api.alertLabels.list)');
     expect(source).toContain('loadAlertNoticeDataFromFacade');
+    expect(source).toContain("import { useRouter, useSearchParams } from 'next/navigation';");
+    expect(source).toContain("receiverSearch: routeParams.get('receiverSearch') ?? ''");
+    expect(source).toContain("ruleSearch: routeParams.get('ruleSearch') ?? ''");
+    expect(source).toContain("templateSearch: routeParams.get('templateSearch') ?? ''");
+    expect(source).toContain("if (!nextState.templatePresetFilter || currentParams.get('templatePreset') === 'true') nextParams.set('templatePreset', String(nextState.templatePresetFilter));");
+    expect(source).toContain('const nextTemplateSearch = templateDraft.name.trim();');
+    expect(source).toContain('setTemplatePresetFilter(false);');
+    expect(source).toContain("templatePresetFilter: false");
+    expect(source).toContain("templateSearch: nextTemplateSearch");
+    expect(source).toContain('return nextParamString ? `/alert/notice?${nextParamString}` : \'/alert/notice\';');
+    expect(source).toContain('router.replace(nextUrl, { scroll: false });');
     expect(source).toContain('receivers: api.alertNotice.receivers.list');
     expect(source).toContain('rules: api.alertNotice.rules.list');
     expect(source).toContain('receiverOptions: api.alertNotice.receivers.options');
@@ -483,6 +518,249 @@ describe('alert notice page', () => {
     expect(source).not.toContain('NoticeListToolbar');
     expect(source).not.toContain('buildNoticeFacts');
     expect(source).not.toContain("from '../../../components/workbench/primitives'");
+  }, 60_000);
+
+  it('initializes notice tab and list queries from URL state', async () => {
+    mockState.currentSearchParams = [
+      'tab=template',
+      'receiverSearch=ops',
+      'receiverPageIndex=2',
+      'receiverPageSize=15',
+      'ruleSearch=severity',
+      'rulePageIndex=1',
+      'rulePageSize=25',
+      'templateSearch=webhook',
+      'templatePreset=false',
+      'templatePageIndex=3',
+      'templatePageSize=15'
+    ].join('&');
+
+    const html = await renderAlertNoticePage();
+
+    expect(html).toContain('data-selected-tab="template"');
+    expect(html).toContain('value="webhook"');
+    expect(html).toContain('value="false" selected=""');
+
+    await mockState.lastLoad?.();
+
+    expect(mockLoadAlertNoticeData).toHaveBeenLastCalledWith(expect.anything(), {
+      receivers: {
+        search: 'ops',
+        pageIndex: 2,
+        pageSize: 15
+      },
+      rules: {
+        search: 'severity',
+        pageIndex: 1,
+        pageSize: 25
+      },
+      templates: {
+        search: 'webhook',
+        preset: false,
+        pageIndex: 3,
+        pageSize: 15
+      }
+    });
+  }, 30_000);
+
+  it('keeps explicitly supplied default notice list URL params during route sync', async () => {
+    const { buildAlertNoticeListRouteUrl } = await import('./alert-notice-page');
+    const nextUrl = buildAlertNoticeListRouteUrl(
+      [
+        'tab=receiver',
+        'receiverSearch=uv_notice_receiver_webhook_token_help_1082',
+        'receiverPageIndex=0',
+        'receiverPageSize=8',
+        'source=notice-receiver-webhook-token-help-1082',
+        'returnTo=%2Falert%2Fsetting%3Fsource%3Dnotice-receiver-webhook-token-help-return-1082%26probe%3Dnotice-receiver-webhook-token-help-1082',
+        'timeRange=last-30m',
+        'live=false',
+        'probe=notice-receiver-webhook-token-help-1082',
+        'templatePreset=true'
+      ].join('&'),
+      'receiver',
+      {
+        selectedTab: 'receiver',
+        receiverSearch: 'uv_notice_receiver_webhook_token_help_1082',
+        receiverPageIndex: 0,
+        receiverPageSize: 8,
+        ruleSearch: '',
+        rulePageIndex: 0,
+        rulePageSize: 8,
+        templateSearch: '',
+        templatePresetFilter: true,
+        templatePageIndex: 0,
+        templatePageSize: 8
+      }
+    );
+
+    expect(nextUrl).toContain('tab=receiver');
+    expect(nextUrl).toContain('receiverSearch=uv_notice_receiver_webhook_token_help_1082');
+    expect(nextUrl).toContain('receiverPageIndex=0');
+    expect(nextUrl).toContain('receiverPageSize=8');
+    expect(nextUrl).toContain('templatePreset=true');
+    expect(nextUrl).toContain('source=notice-receiver-webhook-token-help-1082');
+    expect(nextUrl).toContain('timeRange=last-30m');
+    expect(nextUrl).toContain('live=false');
+    expect(nextUrl).toContain('probe=notice-receiver-webhook-token-help-1082');
+    expect(decodeURIComponent(nextUrl)).toContain('/alert/setting?source=notice-receiver-webhook-token-help-return-1082&probe=notice-receiver-webhook-token-help-1082');
+  }, 30_000);
+
+  it('drops stale non-default notice list URL params when operators switch back to defaults', async () => {
+    const { buildAlertNoticeListRouteUrl } = await import('./alert-notice-page');
+    const nextUrl = buildAlertNoticeListRouteUrl(
+      'tab=template&receiverPageSize=15&templatePreset=false&source=manual',
+      'receiver',
+      {
+        selectedTab: 'receiver',
+        receiverSearch: '',
+        receiverPageIndex: 0,
+        receiverPageSize: 8,
+        ruleSearch: '',
+        rulePageIndex: 0,
+        rulePageSize: 8,
+        templateSearch: '',
+        templatePresetFilter: true,
+        templatePageIndex: 0,
+        templatePageSize: 8
+      }
+    );
+
+    expect(nextUrl).toBe('/alert/notice?source=manual');
+  }, 30_000);
+
+  it('explains notification lane actions before operators create or delete delivery paths', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const receiverHtml = await renderAlertNoticePage();
+    const ruleHtml = await renderAlertNoticePage({
+      signal: 'logs',
+      signalContext: {
+        serviceName: 'checkout',
+        environment: 'prod'
+      }
+    });
+    mockState.currentSearchParams = 'tab=template';
+    const templateHtml = await renderAlertNoticePage();
+
+    expect(source).toContain('function alertNoticeActionHelp');
+    expect(source).toContain('function AlertNoticeActionHelp');
+    for (const id of [
+      'receiver-refresh',
+      'receiver-new',
+      'receiver-delete',
+      'receiver-row-edit',
+      'receiver-row-delete',
+      'receiver-cancel',
+      'receiver-test',
+      'receiver-save',
+      'rule-refresh',
+      'rule-new',
+      'rule-delete',
+      'rule-row-filter-all',
+      'rule-row-enable',
+      'rule-row-edit',
+      'rule-row-delete',
+      'rule-cancel',
+      'rule-save',
+      'template-refresh',
+      'template-new',
+      'template-delete',
+      'template-row-view',
+      'template-row-edit',
+      'template-row-delete',
+      'template-save'
+    ]) {
+      expect(source).toContain(`id="${id}"`);
+      expect(source).toContain(`alertNoticeActionHelp(t, '${id}')`);
+    }
+
+    expect(receiverHtml).toContain('data-alert-notice-action-help="receiver-refresh"');
+    expect(receiverHtml).toContain('data-alert-notice-command-action="receiver-refresh"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help="receiver-new"');
+    expect(receiverHtml).toContain('data-alert-notice-command-action="receiver-new"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help="receiver-delete"');
+    expect(receiverHtml).toContain('data-alert-notice-command-action="receiver-delete"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help="receiver-row-edit"');
+    expect(receiverHtml).toContain('data-alert-notice-command-action="receiver-row-edit"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help="receiver-row-delete"');
+    expect(receiverHtml).toContain('data-alert-notice-command-action="receiver-row-delete"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help-trigger="hertzbeat-ui-action-help"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help-style="icon-after-action"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help-visual="circle-help-icon"');
+    expect(receiverHtml).toContain('data-alert-notice-action-help-icon="lucide-circle-help"');
+    expect(receiverHtml).toContain('lucide-circle-help');
+    expect(receiverHtml).toContain('data-alert-notice-action-help-tooltip="receiver-delete"');
+    expect(receiverHtml).not.toContain('<span aria-hidden="true">?</span>');
+    expect(receiverHtml).toContain(t('alert.notice.action.receiver-new.help'));
+    expect(receiverHtml).toContain(t('alert.notice.action.receiver-delete.impact'));
+    expect(receiverHtml).toContain(t('alert.notice.action.receiver-row-edit.help'));
+    expect(receiverHtml).toContain(t('alert.notice.action.receiver-row-delete.impact'));
+
+    expect(ruleHtml).toContain('data-selected-tab="rule"');
+    expect(ruleHtml).toContain('data-alert-notice-rule-table-actions="sticky-visible-actions"');
+    expect(ruleHtml).toContain('data-alert-notice-action-help="rule-refresh"');
+    expect(ruleHtml).toContain('data-alert-notice-command-action="rule-refresh"');
+    expect(ruleHtml).toContain('data-alert-notice-action-help="rule-new"');
+    expect(ruleHtml).toContain('data-alert-notice-command-action="rule-new"');
+    expect(ruleHtml).toContain('data-alert-notice-action-help="rule-delete"');
+    expect(ruleHtml).toContain('data-alert-notice-command-action="rule-delete"');
+    expect(ruleHtml).toContain('data-alert-notice-action-help="rule-row-filter-all"');
+    expect(ruleHtml).toContain('data-alert-notice-command-action="rule-row-filter-all"');
+    expect(ruleHtml).toContain('data-alert-notice-action-help="rule-row-enable"');
+    expect(ruleHtml).toContain('data-alert-notice-command-action="rule-row-enable"');
+    expect(ruleHtml).toContain('data-alert-notice-action-help="rule-row-edit"');
+    expect(ruleHtml).toContain('data-alert-notice-command-action="rule-row-edit"');
+    expect(ruleHtml).toContain('data-alert-notice-action-help="rule-row-delete"');
+    expect(ruleHtml).toContain('data-alert-notice-command-action="rule-row-delete"');
+    expect(ruleHtml).toContain(t('alert.notice.action.rule-new.impact'));
+    expect(ruleHtml).toContain(t('alert.notice.action.rule-row-enable.help'));
+
+    expect(templateHtml).toContain('data-selected-tab="template"');
+    expect(templateHtml).toContain('data-alert-notice-template-table-actions="sticky-visible-actions"');
+    expect(templateHtml).toContain('data-alert-notice-action-help="template-refresh"');
+    expect(templateHtml).toContain('data-alert-notice-command-action="template-refresh"');
+    expect(templateHtml).toContain('data-alert-notice-action-help="template-new"');
+    expect(templateHtml).toContain('data-alert-notice-command-action="template-new"');
+    expect(templateHtml).toContain('data-alert-notice-action-help="template-delete"');
+    expect(templateHtml).toContain('data-alert-notice-command-action="template-delete"');
+    expect(templateHtml).toContain('data-alert-notice-command-action="template-filter-preset"');
+    expect(templateHtml).toContain('data-alert-notice-command-action="template-row-view"');
+    expect(templateHtml).toContain('data-alert-notice-command-action="template-row-edit"');
+    expect(templateHtml).toContain('data-alert-notice-command-action="template-row-delete"');
+
+    expect(source).toContain('data-alert-notice-action-help={id}');
+    expect(source).toContain('data-alert-notice-command-action={commandAction}');
+    expect(source).toContain('CircleHelp');
+    expect(source).toContain('data-alert-notice-action-help-style="icon-after-action"');
+    expect(source).toContain('data-alert-notice-action-help-visual="circle-help-icon"');
+    expect(source).toContain('data-alert-notice-action-help-icon="lucide-circle-help"');
+    expect(source).toContain('data-alert-notice-action-help-tooltip={id}');
+    expect(source).toContain("alertNoticeActionHelp(t, 'receiver-cancel')");
+    expect(source).toContain('data-alert-notice-command-action="receiver-cancel"');
+    expect(source).toContain("alertNoticeActionHelp(t, 'receiver-test')");
+    expect(source).toContain('data-alert-notice-command-action="receiver-test"');
+    expect(source).toContain("alertNoticeActionHelp(t, 'receiver-save')");
+    expect(source).toContain('data-alert-notice-command-action="receiver-save"');
+    expect(source).toContain("alertNoticeActionHelp(t, 'rule-cancel')");
+    expect(source).toContain('data-alert-notice-command-action="rule-cancel"');
+    expect(source).toContain("alertNoticeActionHelp(t, 'rule-save')");
+    expect(source).toContain('data-alert-notice-command-action="rule-save"');
+    expect(source).toContain('data-alert-notice-command-action="rule-return-to-evidence"');
+    expect(source).toContain("templateReadOnly ? 'template-return' : 'template-cancel'");
+    expect(source).toContain("data-alert-notice-command-action={templateReadOnly ? 'template-return' : 'template-cancel'}");
+    expect(source).toContain("alertNoticeActionHelp(t, templateReadOnly ? 'template-return' : 'template-cancel')");
+    expect(source).toContain("alertNoticeActionHelp(t, 'template-save')");
+    expect(source).toContain('data-alert-notice-command-action="template-save"');
+    expect(source).not.toContain('data-alert-notice-action-help-style="literal-question-after-action"');
+    expect(source).not.toContain('data-alert-notice-action-help-visual="borderless-question"');
+    expect(source).not.toContain('<span aria-hidden="true">?</span>');
+    expect(source).toContain('id="template-refresh"');
+    expect(source).toContain('id="template-new"');
+    expect(source).toContain('id="template-delete"');
+    expect(source).toContain('id="template-row-view"');
+    expect(source).toContain('id="template-row-edit"');
+    expect(source).toContain('id="template-row-delete"');
   }, 30_000);
 
   it('renders missing receiver table settings with the localized empty fallback', async () => {
@@ -613,10 +891,19 @@ describe('alert notice page', () => {
     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="hertzbeat-ui-search-row"');
+    expect(source).toContain('data-alert-notice-rule-empty-action={prefix === \'rule\' ? \'new\' : undefined}');
+    expect(source).toContain('w-[min(560px,70vw)]');
+    expect(source).toContain("label: t('alert.notice.rule.new')");
+    expect(source).toContain('onClick: () => void handleNewRule()');
     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="hertzbeat-ui-dense-table"');
+    expect(source).toContain('data-alert-notice-rule-table-actions="sticky-visible-actions"');
+    expect(source).toContain('const coldStickyActionHeaderClass =');
+    expect(source).toContain('const coldStickyActionCellClass =');
+    expect(source).toContain("<th className={coldStickyActionHeaderClass}>{t('common.edit')}</th>");
+    expect(source).toContain('<td className={coldStickyActionCellClass} onClick={event => event.stopPropagation()}>');
     expect(source).toContain('function NoticeTableSwitch');
     expect(source).toContain('role="switch"');
     expect(source).toContain('data-alert-notice-rule-table-switch={field}');
@@ -633,7 +920,8 @@ describe('alert notice page', () => {
     expect(source).toContain('const ruleId = rule?.id;');
     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('const nextDraft = buildNoticeRuleDraft(detail);');
+    expect(source).toContain('setRuleDraft(nextDraft);');
     expect(source).toContain('onClick={() => void handleEditRule(rule)}');
     expect(source).toContain('field="filter-all"');
     expect(source).toContain('field="enable"');
@@ -774,6 +1062,9 @@ describe('alert notice page', () => {
     expect(source).toContain('data-alert-notice-template-toolbar="hertzbeat-ui-query-toolbar"');
     expect(source).toContain('data-alert-notice-template-toolbar-layout="compact-inline-actions-query"');
     expect(source).toContain('data-alert-notice-template-preset-filter="hertzbeat-ui-select"');
+    expect(source).toContain('data-alert-notice-template-empty-action={prefix === \'template\' ? \'new\' : undefined}');
+    expect(source).toContain("label: t('alert.notice.template.new')");
+    expect(source).toContain('onClick: () => void handleNewTemplate()');
     expect(source).toContain('onSearch={commitTemplateSearch}');
     expect(source).toContain('onClear={templateSearchDraft || templateSearch ? resetTemplateSearch : undefined}');
     expect(source).toContain('setTemplatePageIndex(0);');
@@ -869,7 +1160,8 @@ describe('alert notice page', () => {
     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).toContain('const nextDraft = buildNoticeRuleDraft(detail)');
+    expect(handleEditRuleSource).toContain('setRuleDraft(nextDraft)');
     expect(handleEditRuleSource).not.toContain('buildNoticeRuleDraft(rule)');
   });
 
@@ -901,6 +1193,18 @@ describe('alert notice page', () => {
     expect(handleSaveRuleSource).not.toContain("setRuleError(error instanceof Error ? error.message : t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
   });
 
+  it('clears notice-rule authoring feedback when the draft changes', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleRuleDraftChangeSource = source.slice(source.indexOf('function handleRuleDraftChange('), source.indexOf('async function handleSaveRule()'));
+
+    expect(handleRuleDraftChangeSource).toContain('setRuleDraft(nextDraft);');
+    expect(handleRuleDraftChangeSource).toContain('setRuleMessage(null);');
+    expect(handleRuleDraftChangeSource).toContain('setRuleError(null);');
+    expect(handleRuleDraftChangeSource).toContain('setRuleErrorDetail(null);');
+    expect(source).toContain('onDraftChange={handleRuleDraftChange}');
+    expect(source).not.toContain('onDraftChange={setRuleDraft}');
+  });
+
   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');
... 72101 lines suppressed ...


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