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

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

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

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

commit d068e0a73a1c02c7168b413bed98cb5fbdd50ab1
Author: Logic <zq...@dromara.org>
AuthorDate: Tue Jun 2 21:29:25 2026 +0800

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

diff --git a/.gitignore b/.gitignore
index 8e3b8815b5..02887dff77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,15 @@ node_modules
 .codex-artifacts/
 .playwright-mcp/
 web-next-*.png
+progress.md
+test-results/
+web-next/test-results/
+playwright-report/
+web-next/playwright-report/
+reports/
+screenshots/
+web-next/scripts/topology-scale-proof-seed-lib.mjs
+web-next/scripts/topology-scale-proof-seed-lib.test.ts
 
 # local temp runtime data
 .tmp/
diff --git a/README.md b/README.md
index 0d8cff96a6..3ac9a8b8a8 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,9 @@
 
 ## 🐕 Quick Start   
 
+- Community alpha preview users can start from [docs/alpha-preview.md](docs/alpha-preview.md) for the current
+  Next.js frontend, entity-centered observability, local smoke, and known alpha limitations.
+
 - If you wish to deploy HertzBeat locally, please refer to the following Deployment Documentation for instructions.
 
 ### 🍞 Install HertzBeat   
diff --git a/docs/alpha-preview.md b/docs/alpha-preview.md
new file mode 100644
index 0000000000..a0819467ea
--- /dev/null
+++ b/docs/alpha-preview.md
@@ -0,0 +1,139 @@
+# HertzBeat Community Alpha Preview
+
+This document is for the GitHub community alpha preview branch. It is not a
+stable Apache HertzBeat release checklist. Use it to try the new entity-centered
+observability and Next.js frontend work, report issues, and help harden the
+project toward a beta.
+
+## What Is In Scope
+
+- Entity-centered observability read models for metrics, logs, traces, topology,
+  alerts, owners, runbooks, and handoff context.
+- Next.js `web-next` frontend work for the new shell, entity detail, topology,
+  and UI Lab surfaces.
+- OTLP trace/log/metric query paths backed by the existing warehouse and
+  Greptime integrations.
+- Local demo topology relation seeding for the small Checkout API -> Payment API
+  -> Orders DB example when the `local` Spring profile is active.
+
+## Local Source Quickstart
+
+1. Start the backend from `hertzbeat-startup` with Java 21.
+
+   Keep the Arrow JVM open option when running locally:
+
+   ```shell
+   --add-opens=java.base/java.nio=ALL-UNNAMED
+   ```
+
+   The local backend listens on `http://127.0.0.1:1157` by default.
+
+2. Start the Next.js frontend:
+
+   ```shell
+   cd web-next
+   npm install
+   npm run dev
+   ```
+
+   The preview frontend listens on `http://127.0.0.1:4200`.
+
+3. Open `http://127.0.0.1:4200` and sign in with the local development account
+   configured for your environment.
+
+## Alpha Validation Checklist
+
+Before opening an alpha issue or pull request, run the smallest check that
+matches the area you changed.
+
+Backend entity, topology, observability, and Greptime query changes:
+
+```shell
+./mvnw -pl hertzbeat-manager,hertzbeat-observability,hertzbeat-warehouse \
+  -Dtest=EntityDetailObservabilityReadModelServiceTest,EntityTopologyQueryServiceTest,EntityWorkspaceAccessServiceTest,EntityWorkspaceQueryServiceTest,TraceCallTopologyQueryServiceTest,LocalTopologyDemoRelationSeederTest,LogQueryControllerTest,EntityObservabilityGatewayImplTest,EntityTraceQueryServiceImplTest,GreptimeTraceQueryRepositoryTest,GreptimeDbDataStorageTest \
+  test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false
+```
+
+Startup source package proof:
+
+```shell
+./mvnw -pl hertzbeat-startup -am -DskipTests package
+```
+
+Next.js topology, entity detail, shell, session, and shared UI changes:
+
+```shell
+cd web-next
+npm exec -- vitest run app/topology/page.test.tsx app/ui-lab/page.test.tsx packages/hertzbeat-ui/src/index.test.tsx packages/hertzbeat-ui/src/topology-g6.test.tsx packages/hertzbeat-ui/src/topology-companion.interaction.test.tsx packages/hertzbeat-ui/src/topology-metric-table.interaction.test.tsx components/pages/entity-detail-surface.test.tsx components/shell/app-frame.chrome.test.tsx components/shell/auth-recovery.chrome.test.ts lib/app-frame-state.test.ts lib/entity-contract.test.ts l [...]
+```
+
+Focused frontend lint for touched alpha files:
+
+```shell
+cd web-next
+ESLINT_USE_FLAT_CONFIG=false npm exec -- eslint app components lib packages/hertzbeat-ui/src scripts/topology-g6-browser-smoke.spec.ts test --ext .ts,.tsx
+```
+
+## Optional Local Smoke
+
+The topology G6 browser smoke is intentionally opt-in. It skips unless the
+route and credentials are supplied by the developer:
+
+```shell
+cd web-next
+TOPOLOGY_G6_BROWSER_USERNAME=admin \
+TOPOLOGY_G6_BROWSER_PASSWORD=hertzbeat \
+TOPOLOGY_G6_BROWSER_ROUTE='/topology?environment=prod&timeRange=last-1h&viewMode=service-call&sourceKind=otlp-trace-call&depth=2&groupBy=source-kind' \
+npm exec -- playwright test scripts/topology-g6-browser-smoke.spec.ts
+```
+
+Optional strict assertions can be added with:
+
+- `TOPOLOGY_G6_BROWSER_EXPECTED_NODES`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_EDGES`
+- `TOPOLOGY_G6_BROWSER_FOCUS_NODE_ID`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_RUNTIME_VERSION`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_INITIAL_FIT_STRATEGY`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_WIDE_ZOOM`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_OPERATOR_BOUNDS`
+
+## Local Scale Proof Data
+
+Large topology scale proof data is not seeded by default. The local demo seeder
+only creates the small demo relation repair during normal `local` profile
+startup.
+
+To explicitly seed the local mixed scale proof entity catalog, start the backend
+with:
+
+```shell
+--hertzbeat.topology.local-scale-proof-seed=true
+```
+
+This is intended for local performance investigation only. Do not treat it as
+required alpha setup.
+
+## Known Alpha Limitations
+
+- The Next.js frontend is still an alpha preview and may not yet match every
+  legacy Angular workflow.
+- Topology large-graph behavior is optimized for inspection with render
+  windows, table drilldown, and optional browser smoke. Continue filing cases
+  where real data feels confusing or slow.
+- Runtime verification may depend on local H2 or Greptime data shape. Greptime
+  scale proof fixtures are local-only and should not be committed as generated
+  proof artifacts.
+- Source package rebuilds are expected to pass for the alpha candidate. If a
+  local module or dependency warning blocks packaging, include the focused
+  Maven command and output with the issue report.
+
+## Reporting Issues
+
+When filing alpha feedback, include:
+
+- The route or API endpoint.
+- The storage mode, for example H2-only or Greptime-backed traces.
+- The browser viewport and whether the in-app refresh was used.
+- Expected versus actual node, edge, table, or handoff behavior.
+- Any focused test, Maven, Vitest, ESLint, or browser-smoke command that
+  reproduces the issue.
diff --git a/hertzbeat-ai/pom.xml b/hertzbeat-ai/pom.xml
index 2b9298b6b5..2b270c5bf9 100644
--- a/hertzbeat-ai/pom.xml
+++ b/hertzbeat-ai/pom.xml
@@ -24,7 +24,6 @@
 		<version>2.0-SNAPSHOT</version>
 	</parent>
 	<artifactId>hertzbeat-ai</artifactId>
-	<version>${hertzbeat.version}</version>
 	<properties>
 		<spring-ai.version>1.1.1</spring-ai.version>
 		<java.version>21</java.version>
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java
similarity index 55%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
copy to hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java
index 8bbc0fa5c1..ba0172cb53 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java
@@ -15,47 +15,28 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.observability.dto.handoff;
+package org.apache.hertzbeat.common.observability.dto.entity;
 
+import java.util.Collections;
 import java.util.List;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
-import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
+import org.apache.hertzbeat.common.observability.dto.log.EntityLogQueryHint;
 import org.apache.hertzbeat.common.observability.dto.log.EntityLogSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceQueryHintDto;
 import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceSummaryDto;
-import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
 
 /**
- * Request payload for building entity response handoffs.
+ * Shared entity evidence payload consumed by signal handoffs and entity detail surfaces.
  */
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-public class EntityResponseHandoffsRequest {
-
-    private String returnTo;
-
-    private String returnLabel;
-
-    private String entityOwner;
-
-    private String entitySystem;
-
-    private String entityEnvironment;
-
-    private String entitySource;
-
-    private ObservedEntityContext entityContext;
-
-    private List<SingleAlert> activeAlerts;
-
-    private List<Monitor> monitors;
+public class EntitySignalEvidenceBundle {
 
     private EntityLogSummaryInfo logSummary;
 
@@ -67,13 +48,30 @@ public class EntityResponseHandoffsRequest {
 
     private List<TraceEvidence> traceEvidence;
 
-    private List<EntityTraceQueryHintDto> traceQueryHints;
-
-    private boolean ownerReady;
-
-    private boolean runbookReady;
+    private List<EntityLogQueryHint> logQueryHints;
 
-    private boolean relationReady;
+    private List<EntityTraceQueryHintDto> traceQueryHints;
 
-    private boolean telemetryReady;
+    private EntityUnifiedEvidenceSummary unifiedEvidenceSummary;
+
+    private EntityTriageRecommendation triageRecommendation;
+
+    public static EntitySignalEvidenceBundle from(EntityObservabilityDetailBundle detailBundle) {
+        if (detailBundle == null) {
+            return new EntitySignalEvidenceBundle(
+                    null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
+                    Collections.emptyList(), Collections.emptyList(), null, null);
+        }
+        return new EntitySignalEvidenceBundle(
+                detailBundle.getLogSummary(),
+                detailBundle.getTraceSummary(),
+                detailBundle.getMetricEvidence(),
+                detailBundle.getLogEvidence(),
+                detailBundle.getTraceEvidence(),
+                detailBundle.getLogQueryHints(),
+                detailBundle.getTraceQueryHints(),
+                detailBundle.getUnifiedEvidenceSummary(),
+                detailBundle.getTriageRecommendation()
+        );
+    }
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
index 8bbc0fa5c1..99e9f64326 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
@@ -23,6 +23,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
@@ -69,6 +70,8 @@ public class EntityResponseHandoffsRequest {
 
     private List<EntityTraceQueryHintDto> traceQueryHints;
 
+    private EntitySignalEvidenceBundle signalEvidence;
+
     private boolean ownerReady;
 
     private boolean runbookReady;
@@ -76,4 +79,29 @@ public class EntityResponseHandoffsRequest {
     private boolean relationReady;
 
     private boolean telemetryReady;
+
+    public EntityResponseHandoffsRequest(String returnTo,
+                                         String returnLabel,
+                                         String entityOwner,
+                                         String entitySystem,
+                                         String entityEnvironment,
+                                         String entitySource,
+                                         ObservedEntityContext entityContext,
+                                         List<SingleAlert> activeAlerts,
+                                         List<Monitor> monitors,
+                                         EntityLogSummaryInfo logSummary,
+                                         EntityTraceSummaryDto traceSummary,
+                                         List<MetricEvidence> metricEvidence,
+                                         List<LogEvidence> logEvidence,
+                                         List<TraceEvidence> traceEvidence,
+                                         List<EntityTraceQueryHintDto> traceQueryHints,
+                                         boolean ownerReady,
+                                         boolean runbookReady,
+                                         boolean relationReady,
+                                         boolean telemetryReady) {
+        this(returnTo, returnLabel, entityOwner, entitySystem, entityEnvironment, entitySource,
+                entityContext, activeAlerts, monitors, logSummary, traceSummary, metricEvidence,
+                logEvidence, traceEvidence, traceQueryHints, null, ownerReady, runbookReady,
+                relationReady, telemetryReady);
+    }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
index a004b00c4a..9bd509760b 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.manager.dao;
 import java.util.List;
 import java.util.Optional;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -39,4 +40,6 @@ public interface ObserveEntityDao extends JpaRepository<ObserveEntity, Long>, Jp
     Optional<ObserveEntity> findFirstByWorkspaceIdAndTypeAndName(String workspaceId, String type, String name);
 
     List<ObserveEntity> findAllByWorkspaceId(String workspaceId, Sort sort);
+
+    List<ObserveEntity> findAllByWorkspaceId(String workspaceId, Pageable pageable);
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java
index 1a79548e36..3b8f9c93a1 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java
@@ -27,6 +27,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityEvidenceSummar
 import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -69,6 +70,8 @@ public class EntityDetailDto {
 
     private List<TraceEvidence> traceEvidence;
 
+    private EntitySignalEvidenceBundle signalEvidence;
+
     private EntityUnifiedEvidenceSummary unifiedEvidenceSummary;
 
     private EntityTriageRecommendation triageRecommendation;
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
index 294797e37e..56d8907c5f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
@@ -27,6 +27,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -130,6 +131,7 @@ public class EntityDetailObservabilityReadModelService {
         EntityLogSummaryInfo logSummary = entityObservabilityGateway.buildEntityLogSummary(logQueryHints);
         EntityObservabilityDetailBundle observabilityDetail = entityObservabilityGateway.resolveEntityDetailBundle(
                 entityContext, statusInfo, evidenceSummary, monitorSummary, logSummary, monitors, logQueryHints);
+        EntitySignalEvidenceBundle signalEvidence = EntitySignalEvidenceBundle.from(observabilityDetail);
         logSummary = observabilityDetail.getLogSummary();
         logQueryHints = observabilityDetail.getLogQueryHints();
         EntityTraceSummaryDto traceSummary = observabilityDetail.getTraceSummary();
@@ -147,10 +149,8 @@ public class EntityDetailObservabilityReadModelService {
                         entityDto.getEntity(), evidenceSummary, logSummary, opsSummary);
         EntityStatusPageSummaryInfo statusPageSummary =
                 entityObservabilityGateway.buildEntityStatusPageSummary(entity, opsSummary);
-        EntityResponseHandoffsInfo responseHandoffs =
-                entityResponseHandoffReadModelService.buildResponseHandoffs(
-                        entityId, entityContext, activeAlerts, monitors, logSummary, traceSummary,
-                        metricEvidence, logEvidence, traceEvidence, traceQueryHints, opsSummary);
+        EntityResponseHandoffsInfo responseHandoffs = entityResponseHandoffReadModelService.buildResponseHandoffs(
+                entityId, entityContext, activeAlerts, monitors, signalEvidence, opsSummary);
         EntityDetailDto.EntityNoiseControlSummaryInfo noiseControlSummary =
                 resolveRequestWorkspaceAtEvidenceBoundaries
                         ? entityNoiseControlReadModelService.buildNoiseControlSummary(entityDto, monitors, activeAlerts)
@@ -161,7 +161,7 @@ public class EntityDetailObservabilityReadModelService {
                         ? entityActivityReadModelService.getDefinitionActivities(entityId, 12)
                         : entityActivityReadModelService.getDefinitionActivities(entityId, 12, requestWorkspaceId);
         return new EntityDetailDto(entityDto, statusInfo, evidenceSummary, alertSummary, monitorSummary, logSummary,
-                traceSummary, metricEvidence, logEvidence, traceEvidence, unifiedEvidenceSummary,
+                traceSummary, metricEvidence, logEvidence, traceEvidence, signalEvidence, unifiedEvidenceSummary,
                 triageRecommendation, opsSummary, nextActions, statusPageSummary, responseHandoffs,
                 noiseControlSummary, boundMonitors, activeAlerts, logQueryHints, traceQueryHints,
                 definitionActivities);
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
index 924e62d8f5..bd04b12ca7 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
@@ -21,6 +21,7 @@ import java.util.List;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
@@ -49,13 +50,11 @@ public class EntityResponseHandoffReadModelService {
                                                             ObservedEntityContext entityContext,
                                                             List<SingleAlert> activeAlerts,
                                                             List<Monitor> monitors,
-                                                            EntityLogSummaryInfo logSummary,
-                                                            EntityTraceSummaryDto traceSummary,
-                                                            List<MetricEvidence> metricEvidence,
-                                                            List<LogEvidence> logEvidence,
-                                                            List<TraceEvidence> traceEvidence,
-                                                            List<EntityTraceQueryHintDto> traceQueryHints,
+                                                            EntitySignalEvidenceBundle signalEvidence,
                                                             EntityOpsSummaryInfo opsSummary) {
+        EntitySignalEvidenceBundle safeEvidence = signalEvidence == null
+                ? new EntitySignalEvidenceBundle()
+                : signalEvidence;
         return entityObservabilityGateway.buildEntityResponseHandoffs(new EntityResponseHandoffsRequest(
                 "/entities/" + entityId,
                 entityObservabilityGateway.buildEntityReturnLabel(entityContext),
@@ -67,16 +66,34 @@ public class EntityResponseHandoffReadModelService {
                 entityContext,
                 activeAlerts,
                 monitors,
-                logSummary,
-                traceSummary,
-                metricEvidence,
-                logEvidence,
-                traceEvidence,
-                traceQueryHints,
+                safeEvidence.getLogSummary(),
+                safeEvidence.getTraceSummary(),
+                safeEvidence.getMetricEvidence(),
+                safeEvidence.getLogEvidence(),
+                safeEvidence.getTraceEvidence(),
+                safeEvidence.getTraceQueryHints(),
+                safeEvidence,
                 opsSummary != null && opsSummary.isOwnerReady(),
                 opsSummary != null && opsSummary.isRunbookReady(),
                 opsSummary != null && opsSummary.isRelationReady(),
                 opsSummary != null && opsSummary.isTelemetryReady()
         ));
     }
+
+    public EntityResponseHandoffsInfo buildResponseHandoffs(long entityId,
+                                                            ObservedEntityContext entityContext,
+                                                            List<SingleAlert> activeAlerts,
+                                                            List<Monitor> monitors,
+                                                            EntityLogSummaryInfo logSummary,
+                                                            EntityTraceSummaryDto traceSummary,
+                                                            List<MetricEvidence> metricEvidence,
+                                                            List<LogEvidence> logEvidence,
+                                                            List<TraceEvidence> traceEvidence,
+                                                            List<EntityTraceQueryHintDto> traceQueryHints,
+                                                            EntityOpsSummaryInfo opsSummary) {
+        return buildResponseHandoffs(entityId, entityContext, activeAlerts, monitors,
+                new EntitySignalEvidenceBundle(logSummary, traceSummary, metricEvidence, logEvidence, traceEvidence,
+                        null, traceQueryHints, null, null),
+                opsSummary);
+    }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java
index 51de579a8f..fc5a62141f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java
@@ -33,6 +33,7 @@ import org.apache.hertzbeat.common.entity.manager.EntityRelation;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.apache.hertzbeat.manager.pojo.dto.EntityTopologyGraphInfo;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
@@ -45,7 +46,7 @@ public class EntityTopologyQueryService {
 
     private static final int DEFAULT_DEPTH = 1;
     private static final int MAX_DEPTH = 2;
-    private static final int DEFAULT_TOPOLOGY_ENTITY_LIMIT = 12;
+    private static final int DEFAULT_TOPOLOGY_ENTITY_LIMIT = 64;
     private static final int MAX_TOPOLOGY_EDGE_PAGE_SIZE = 200;
     private static final int IMPACT_TIMELINE_LIMIT = 12;
     private static final String SOURCE_KIND_ENTITY_RELATION = "entity-relation";
@@ -111,7 +112,7 @@ public class EntityTopologyQueryService {
         graph.setApiBacked(true);
         graph.setFocusEntityId(focusEntityId);
         graph.setDepth(depth);
-        graph.setSourceKinds(List.of(normalizedSourceKind));
+        graph.setSourceKinds(List.of());
         if (focusEntityId == null) {
             return buildDefaultTopologyGraph(graph, depth, environment, normalizedSourceKind,
                     sourceSelection, start, end, normalizedRelationType, hideInternal, pageIndex, pageSize);
@@ -145,10 +146,10 @@ public class EntityTopologyQueryService {
                 ? collectMonitorBinds(entityById.keySet())
                 : List.of();
         Map<Long, Monitor> monitorById = monitorMap(monitorBinds);
-        graph.setSourceKinds(buildSourceKinds(normalizedSourceKind, monitorBinds, traceCallEdges));
         List<EntityTopologyGraphInfo.Edge> visibleEdges = filterAndPageEdges(
                 buildEdges(relations, entityById.keySet(), monitorBinds, monitorById, traceCallEdges),
                 normalizedRelationType, hideInternal, pageIndex, pageSize);
+        graph.setSourceKinds(buildSourceKinds(normalizedSourceKind, visibleEdges));
         graph.setNodes(filterNodesByVisibleEdges(
                 buildNodes(entityById, monitorBinds, monitorById, focusEntityId, traceCallEdges),
                 visibleEdges, focusEntityId, normalizedRelationType, pageSize));
@@ -186,7 +187,7 @@ public class EntityTopologyQueryService {
         }
 
         TraceCallTopologyReadModel traceCallReadModel = sourceSelection.includeTraceCalls()
-                ? traceCallTopologyReadModel(entityById.values(), environment, start, end, hideInternal)
+                ? traceCallTopologyOverviewReadModel(environment, start, end, hideInternal)
                 : TraceCallTopologyReadModel.empty();
         Map<Long, ObserveEntity> traceEntityById = traceCallReadModel.entityById();
         List<TraceCallTopologyEdgeInfo> traceCallEdges = traceCallReadModel.edges();
@@ -200,10 +201,10 @@ public class EntityTopologyQueryService {
                 ? collectMonitorBinds(entityById.keySet())
                 : List.of();
         Map<Long, Monitor> monitorById = monitorMap(monitorBinds);
-        graph.setSourceKinds(buildSourceKinds(sourceKind, monitorBinds, traceCallEdges));
         List<EntityTopologyGraphInfo.Edge> visibleEdges = filterAndPageEdges(
                 buildEdges(relations, entityById.keySet(), monitorBinds, monitorById, traceCallEdges),
                 relationType, hideInternal, pageIndex, pageSize);
+        graph.setSourceKinds(buildSourceKinds(sourceKind, visibleEdges));
         graph.setNodes(filterNodesByVisibleEdges(
                 buildNodes(entityById, monitorBinds, monitorById, null, traceCallEdges),
                 visibleEdges, null, relationType, pageSize));
@@ -214,14 +215,14 @@ public class EntityTopologyQueryService {
 
     private List<ObserveEntity> defaultSeedEntities(String environment) {
         Sort sort = Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id"));
-        List<ObserveEntity> entities = entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(sort);
+        List<ObserveEntity> entities = entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(
+                PageRequest.of(0, DEFAULT_TOPOLOGY_ENTITY_LIMIT, sort));
         if (entities == null) {
             return List.of();
         }
         return entities.stream()
                 .filter(entity -> entity.getId() != null)
                 .filter(entity -> matchesEnvironment(entity, environment))
-                .limit(DEFAULT_TOPOLOGY_ENTITY_LIMIT)
                 .toList();
     }
 
@@ -235,6 +236,15 @@ public class EntityTopologyQueryService {
         return readModel == null ? TraceCallTopologyReadModel.empty() : readModel;
     }
 
+    private TraceCallTopologyReadModel traceCallTopologyOverviewReadModel(String environment,
+                                                                          Long start,
+                                                                          Long end,
+                                                                          Boolean hideInternal) {
+        TraceCallTopologyReadModel readModel = traceCallTopologyQueryService.findTraceCallEdgesForOverview(
+                environment, start, end, hideInternal == null || hideInternal);
+        return readModel == null ? TraceCallTopologyReadModel.empty() : readModel;
+    }
+
     private TopologySourceSelection sourceSelection(String sourceKind) {
         if (!StringUtils.hasText(sourceKind)) {
             return TopologySourceSelection.all();
@@ -329,19 +339,41 @@ public class EntityTopologyQueryService {
     }
 
     private List<String> buildSourceKinds(String requestedSourceKind,
-                                          Collection<EntityMonitorBind> monitorBinds,
-                                          Collection<TraceCallTopologyEdgeInfo> traceCallEdges) {
+                                          Collection<EntityTopologyGraphInfo.Edge> visibleEdges) {
+        if (visibleEdges == null || visibleEdges.isEmpty()) {
+            return List.of();
+        }
         Set<String> sourceKinds = new LinkedHashSet<>();
         sourceKinds.add(requestedSourceKind);
-        if (!monitorBinds.isEmpty()) {
-            sourceKinds.add(SOURCE_KIND_MONITOR_BIND);
-        }
-        if (!traceCallEdges.isEmpty()) {
-            sourceKinds.add(SOURCE_KIND_OTLP_TRACE_CALL);
+        for (EntityTopologyGraphInfo.Edge edge : visibleEdges) {
+            if (edge == null) {
+                continue;
+            }
+            addSourceKindsFromEdgeEvidence(sourceKinds, edge.getEvidenceBadges());
+            addSourceKindFromEdgeSource(sourceKinds, edge.getRelationSource());
         }
         return new ArrayList<>(sourceKinds);
     }
 
+    private void addSourceKindsFromEdgeEvidence(Set<String> sourceKinds, Collection<String> evidenceBadges) {
+        if (evidenceBadges == null) {
+            return;
+        }
+        evidenceBadges.forEach(source -> addSourceKindFromEdgeSource(sourceKinds, source));
+    }
+
+    private void addSourceKindFromEdgeSource(Set<String> sourceKinds, String source) {
+        if (!StringUtils.hasText(source)) {
+            return;
+        }
+        String normalized = source.trim().toLowerCase();
+        if (SOURCE_KIND_ENTITY_RELATION.equals(normalized)
+                || SOURCE_KIND_MONITOR_BIND.equals(normalized)
+                || SOURCE_KIND_OTLP_TRACE_CALL.equals(normalized)) {
+            sourceKinds.add(normalized);
+        }
+    }
+
     private Set<Long> collectEntityIds(Long focusEntityId, Collection<EntityRelation> relations) {
         return collectEntityIds(List.of(focusEntityId), relations);
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java
index a735c8dea7..a71d0bf0cd 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java
@@ -27,6 +27,7 @@ import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
 import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
@@ -77,10 +78,20 @@ public class EntityWorkspaceAccessService {
         return filterEntitiesByRequestWorkspace(entities, normalizedWorkspaceId);
     }
 
+    public List<ObserveEntity> findAccessibleEntities(String requestWorkspaceId, Pageable pageable) {
+        String normalizedWorkspaceId = normalizeWorkspaceId(requestWorkspaceId);
+        List<ObserveEntity> entities = entityWorkspaceQueryService.findEntities(normalizedWorkspaceId, pageable);
+        return filterEntitiesByRequestWorkspace(entities, normalizedWorkspaceId);
+    }
+
     public List<ObserveEntity> findAccessibleEntitiesForRequestWorkspace(Sort sort) {
         return findAccessibleEntities(currentRequestWorkspaceId(), sort);
     }
 
+    public List<ObserveEntity> findAccessibleEntitiesForRequestWorkspace(Pageable pageable) {
+        return findAccessibleEntities(currentRequestWorkspaceId(), pageable);
+    }
+
     public Optional<ObserveEntity> findEntityById(long entityId) {
         return entityWorkspaceQueryService.findEntityById(entityId);
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java
index 3a467c2c48..25fa541198 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java
@@ -52,6 +52,13 @@ public class EntityWorkspaceQueryService {
         return observeEntityDao.findAll(sort);
     }
 
+    public List<ObserveEntity> findEntities(String workspaceId, Pageable pageable) {
+        if (StringUtils.hasText(workspaceId)) {
+            return observeEntityDao.findAllByWorkspaceId(workspaceId, pageable);
+        }
+        return observeEntityDao.findAll(pageable).getContent();
+    }
+
     public Page<ObserveEntity> findEntityPage(Specification<ObserveEntity> specification, Pageable pageable) {
         return observeEntityDao.findAll(specification, pageable);
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeeder.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeeder.java
new file mode 100644
index 0000000000..1b2e965a4b
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeeder.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
+import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.apache.hertzbeat.manager.dao.ObserveEntityDao;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Seeds local-only demo topology relations so fresh local H2 catalogs do not render node-only graphs.
+ */
+@Component
+@Profile("local")
+@Slf4j
+public class LocalTopologyDemoRelationSeeder implements ApplicationRunner {
+
+    private static final String MIXED_SCALE_PROOF_SEED_OPTION = "hertzbeat.topology.local-scale-proof-seed";
+    private static final String WORKSPACE_ID = AuthTokenScopes.DEFAULT_WORKSPACE_ID;
+    private static final String NAMESPACE = "commerce";
+    private static final String ENVIRONMENT = "prod";
+    private static final String SEED_NAME = "local-topology-demo-seed";
+    private static final String MIXED_PROOF_BATCH = "hb-mix-1780329856";
+    private static final String MIXED_PROOF_NAMESPACE = "scale-mix";
+    private static final long MIXED_PROOF_ENTITY_ID_BASE = 646566130000000L;
+
+    private final ObserveEntityDao observeEntityDao;
+    private final EntityRelationDao entityRelationDao;
+    private final EntityIdentityDao entityIdentityDao;
+
+    public LocalTopologyDemoRelationSeeder(ObserveEntityDao observeEntityDao,
+                                           EntityRelationDao entityRelationDao,
+                                           EntityIdentityDao entityIdentityDao) {
+        this.observeEntityDao = observeEntityDao;
+        this.entityRelationDao = entityRelationDao;
+        this.entityIdentityDao = entityIdentityDao;
+    }
+
+    @Override
+    @Transactional
+    public void run(ApplicationArguments args) {
+        seedDemoRelations();
+        if (isMixedScaleProofSeedEnabled(args)) {
+            seedMixedScaleProofEntities();
+        } else {
+            log.info("Skipped local mixed topology scale proof seed. Enable with --{}=true.",
+                    MIXED_SCALE_PROOF_SEED_OPTION);
+        }
+    }
+
+    boolean isMixedScaleProofSeedEnabled(ApplicationArguments args) {
+        if (args == null || !args.containsOption(MIXED_SCALE_PROOF_SEED_OPTION)) {
+            return false;
+        }
+        List<String> values = args.getOptionValues(MIXED_SCALE_PROOF_SEED_OPTION);
+        return values != null && values.stream().anyMatch(value -> "true".equalsIgnoreCase(value));
+    }
+
+    void seedDemoRelations() {
+        Optional<ObserveEntity> checkout = findDemoEntity("service", "Checkout API");
+        Optional<ObserveEntity> payment = findDemoEntity("service", "Payment API");
+        Optional<ObserveEntity> orders = findDemoEntity("database", "Orders DB");
+        if (checkout.isEmpty() || payment.isEmpty() || orders.isEmpty()) {
+            return;
+        }
+
+        List<EntityRelation> missingRelations = new ArrayList<>();
+        if (!relationExists(checkout.get().getId(), payment.get().getId(), "trace-call")) {
+            missingRelations.add(EntityRelation.builder()
+                    .sourceEntityId(checkout.get().getId())
+                    .targetEntityId(payment.get().getId())
+                    .targetRef("service:commerce/Payment API")
+                    .relationType("trace-call")
+                    .relationSource("otlp-trace-call")
+                    .status("confirmed")
+                    .score(96)
+                    .description("Checkout API calls Payment API in the local topology demo.")
+                    .attributes(seedAttributes("outbound"))
+                    .build());
+        }
+        if (!relationExists(payment.get().getId(), orders.get().getId(), "depends_on")) {
+            missingRelations.add(EntityRelation.builder()
+                    .sourceEntityId(payment.get().getId())
+                    .targetEntityId(orders.get().getId())
+                    .targetRef("database:commerce/Orders DB")
+                    .relationType("depends_on")
+                    .relationSource("cmdb-manual-label")
+                    .status("confirmed")
+                    .score(94)
+                    .description("Payment API depends on Orders DB in the local topology demo.")
+                    .attributes(seedAttributes("outbound"))
+                    .build());
+        }
+        if (!missingRelations.isEmpty()) {
+            entityRelationDao.saveAll(missingRelations);
+            log.info("Seeded {} local topology demo relation(s).", missingRelations.size());
+        }
+    }
+
+    void seedMixedScaleProofEntities() {
+        List<ObserveEntity> missingEntities = new ArrayList<>();
+        List<EntityIdentity> missingIdentities = new ArrayList<>();
+        List<String> serviceNames = mixedScaleProofServiceNames();
+
+        for (int index = 0; index < serviceNames.size(); index += 1) {
+            String serviceName = serviceNames.get(index);
+            Optional<ObserveEntity> existingEntity = findMixedScaleProofEntity(serviceName);
+            if (existingEntity.isPresent()) {
+                if (!hasServiceNameIdentity(existingEntity.get().getId(), serviceName)) {
+                    missingIdentities.add(serviceNameIdentity(existingEntity.get().getId(), serviceName));
+                }
+                continue;
+            }
+            long entityId = MIXED_PROOF_ENTITY_ID_BASE + index;
+            missingEntities.add(ObserveEntity.builder()
+                    .id(entityId)
+                    .workspaceId(WORKSPACE_ID)
+                    .type("service")
+                    .namespace(MIXED_PROOF_NAMESPACE)
+                    .name(serviceName)
+                    .displayName(displayName(serviceName))
+                    .environment(ENVIRONMENT)
+                    .status("unknown")
+                    .criticality("medium")
+                    .owner("local-scale-proof")
+                    .lifecycle("local")
+                    .build());
+            missingIdentities.add(serviceNameIdentity(entityId, serviceName));
+        }
+
+        if (!missingEntities.isEmpty()) {
+            observeEntityDao.saveAll(missingEntities);
+            log.info("Seeded {} local mixed topology scale proof entity row(s).", missingEntities.size());
+        }
+        if (!missingIdentities.isEmpty()) {
+            entityIdentityDao.saveAll(missingIdentities);
+            log.info("Seeded {} local mixed topology scale proof service.name identity row(s).",
+                    missingIdentities.size());
+        }
+    }
+
+    private boolean hasServiceNameIdentity(Long entityId, String serviceName) {
+        return entityIdentityDao.findAllByEntityIdOrderByPriorityDescIdAsc(entityId)
+                .stream()
+                .anyMatch(identity -> "service.name".equals(identity.getIdentityKey())
+                        && serviceName.equals(identity.getIdentityValue()));
+    }
+
+    private EntityIdentity serviceNameIdentity(Long entityId, String serviceName) {
+        return EntityIdentity.builder()
+                .entityId(entityId)
+                .identityType("otel_resource")
+                .identityKey("service.name")
+                .identityValue(serviceName)
+                .normalizedValue(serviceName.toLowerCase(java.util.Locale.ROOT))
+                .priority(100)
+                .primaryIdentity(true)
+                .build();
+    }
+
+    private Optional<ObserveEntity> findDemoEntity(String type, String name) {
+        return observeEntityDao
+                .findFirstByWorkspaceIdAndTypeAndNamespaceAndName(WORKSPACE_ID, type, NAMESPACE, name)
+                .filter(entity -> ENVIRONMENT.equals(entity.getEnvironment()));
+    }
+
+    private Optional<ObserveEntity> findMixedScaleProofEntity(String name) {
+        return observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                WORKSPACE_ID, "service", MIXED_PROOF_NAMESPACE, name);
+    }
+
+    private List<String> mixedScaleProofServiceNames() {
+        List<String> serviceNames = new ArrayList<>();
+        serviceNames.add(MIXED_PROOF_BATCH + "-edge-gateway");
+        for (int domainIndex = 0; domainIndex < 12; domainIndex += 1) {
+            serviceNames.add("%s-domain-%02d".formatted(MIXED_PROOF_BATCH, domainIndex));
+        }
+        for (int domainIndex = 0; domainIndex < 12; domainIndex += 1) {
+            for (int serviceIndex = 0; serviceIndex < 165; serviceIndex += 1) {
+                serviceNames.add("%s-svc-%02d-%03d".formatted(MIXED_PROOF_BATCH, domainIndex, serviceIndex));
+            }
+        }
+        return serviceNames;
+    }
+
+    private String displayName(String serviceName) {
+        if (serviceName.equals(MIXED_PROOF_BATCH + "-edge-gateway")) {
+            return "Mixed Edge Gateway";
+        }
+        if (serviceName.contains("-domain-")) {
+            return "Domain " + serviceName.substring(serviceName.lastIndexOf('-') + 1) + " Gateway";
+        }
+        return serviceName;
+    }
+
+    private boolean relationExists(Long sourceEntityId, Long targetEntityId, String relationType) {
+        return entityRelationDao.findBySourceEntityIdOrTargetEntityId(sourceEntityId, sourceEntityId)
+                .stream()
+                .anyMatch(relation -> sourceEntityId.equals(relation.getSourceEntityId())
+                        && targetEntityId.equals(relation.getTargetEntityId())
+                        && relationType.equals(relation.getRelationType()));
+    }
+
+    private Map<String, String> seedAttributes(String direction) {
+        return Map.of(
+                "seed", SEED_NAME,
+                "direction", direction
+        );
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java
index dd02aa33c7..50e9a5002f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java
@@ -102,14 +102,30 @@ public class TraceCallTopologyQueryService {
         if (CollectionUtils.isEmpty(seedEntities)) {
             return TraceCallTopologyReadModel.empty();
         }
+        return findTraceCallEdges(seedEntities, environment, start, end, hideInternal, true);
+    }
+
+    public TraceCallTopologyReadModel findTraceCallEdgesForOverview(String environment,
+                                                                    Long start,
+                                                                    Long end,
+                                                                    Boolean hideInternal) {
+        return findTraceCallEdges(Collections.emptyList(), environment, start, end, hideInternal, false);
+    }
+
+    private TraceCallTopologyReadModel findTraceCallEdges(Collection<ObserveEntity> seedEntities,
+                                                         String environment,
+                                                         Long start,
+                                                         Long end,
+                                                         Boolean hideInternal,
+                                                         boolean scopeToSeedEntities) {
         boolean shouldHideInternal = hideInternal == null || hideInternal;
         TraceCallTopologyReadModel serviceGraphReadModel =
-                findServiceGraphReadModel(seedEntities, environment, start, end, shouldHideInternal);
+                findServiceGraphReadModel(seedEntities, environment, start, end, shouldHideInternal,
+                        scopeToSeedEntities);
         if (!serviceGraphReadModel.edges().isEmpty()) {
             return serviceGraphReadModel;
         }
-        List<TraceSpanRow> spans = safeList(traceQueryRepository.queryRecentTraceRows(
-                        TRACE_ROW_LIMIT, start, end, null, environment, shouldHideInternal))
+        List<TraceSpanRow> spans = queryRecentTraceRows(start, end, environment, shouldHideInternal)
                 .stream()
                 .map(this::toTraceSpanRow)
                 .filter(Objects::nonNull)
@@ -128,7 +144,7 @@ public class TraceCallTopologyQueryService {
         Map<Long, ObserveEntity> entityById = new LinkedHashMap<>();
         entityByService.values().forEach(entity -> entityById.putIfAbsent(entity.getId(), entity));
         List<TraceCallTopologyEdgeInfo> edges = buildTraceCallEdgesFromSpans(
-                spans, entityByService, seedEntities, start, end);
+                spans, entityByService, seedEntities, start, end, scopeToSeedEntities);
         return new TraceCallTopologyReadModel(entityById, edges);
     }
 
@@ -136,8 +152,9 @@ public class TraceCallTopologyQueryService {
                                                                  String environment,
                                                                  Long start,
                                                                  Long end,
-                                                                 Boolean hideInternal) {
-        Set<String> seedServiceNames = seedServiceNames(seedEntities);
+                                                                 Boolean hideInternal,
+                                                                 boolean scopeToSeedEntities) {
+        Set<String> seedServiceNames = scopeToSeedEntities ? seedServiceNames(seedEntities) : Collections.emptySet();
         List<TraceServiceGraphRow> serviceGraphRows = queryServiceGraphRows(
                 start, end, environment, seedServiceNames, hideInternal);
         if (serviceGraphRows.isEmpty()) {
@@ -151,7 +168,7 @@ public class TraceCallTopologyQueryService {
         Map<String, ObserveEntity> entityByService = resolveEntityByService(seedEntities, observedServices,
                 environment);
         List<TraceCallTopologyEdgeInfo> edges = buildTraceCallEdgesFromServiceGraphRows(
-                serviceGraphRows, entityByService, seedEntities, start, end);
+                serviceGraphRows, entityByService, seedEntities, start, end, scopeToSeedEntities);
         Map<Long, ObserveEntity> entityById = new LinkedHashMap<>();
         for (TraceCallTopologyEdgeInfo edge : edges) {
             ObserveEntity source = findEntityById(entityByService.values(), edge.sourceEntityId());
@@ -187,6 +204,22 @@ public class TraceCallTopologyQueryService {
         }
     }
 
+    private List<Map<String, Object>> queryRecentTraceRows(Long start,
+                                                           Long end,
+                                                           String environment,
+                                                           Boolean hideInternal) {
+        try {
+            return CompletableFuture.supplyAsync(() -> safeList(traceQueryRepository.queryRecentTraceRows(
+                            TRACE_ROW_LIMIT, start, end, null, environment, hideInternal)))
+                    .get(serviceGraphQueryTimeout.toMillis(), TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            return Collections.emptyList();
+        } catch (ExecutionException | TimeoutException | RuntimeException ex) {
+            return Collections.emptyList();
+        }
+    }
+
     private Set<String> seedServiceNames(Collection<ObserveEntity> seedEntities) {
         Set<String> serviceNames = new LinkedHashSet<>();
         for (ObserveEntity entity : safeList(seedEntities)) {
@@ -305,7 +338,8 @@ public class TraceCallTopologyQueryService {
                                                                          Map<String, ObserveEntity> entityByService,
                                                                          Collection<ObserveEntity> seedEntities,
                                                                          Long start,
-                                                                         Long end) {
+                                                                         Long end,
+                                                                         boolean scopeToSeedEntities) {
         Map<String, TraceSpanRow> spanByTraceAndSpan = new LinkedHashMap<>();
         for (TraceSpanRow span : spans) {
             spanByTraceAndSpan.put(spanKey(span.traceId(), span.spanId()), span);
@@ -329,7 +363,9 @@ public class TraceCallTopologyQueryService {
             if (source == null || target == null || Objects.equals(source.getId(), target.getId())) {
                 continue;
             }
-            if (!seedEntityIds.contains(source.getId()) && !seedEntityIds.contains(target.getId())) {
+            if (scopeToSeedEntities
+                    && !seedEntityIds.contains(source.getId())
+                    && !seedEntityIds.contains(target.getId())) {
                 continue;
             }
             String edgeKey = source.getId() + ":" + target.getId();
@@ -346,7 +382,8 @@ public class TraceCallTopologyQueryService {
             Map<String, ObserveEntity> entityByService,
             Collection<ObserveEntity> seedEntities,
             Long start,
-            Long end) {
+            Long end,
+            boolean scopeToSeedEntities) {
         Set<Long> seedEntityIds = seedEntities.stream()
                 .filter(Objects::nonNull)
                 .map(ObserveEntity::getId)
@@ -359,7 +396,9 @@ public class TraceCallTopologyQueryService {
             if (source == null || target == null || Objects.equals(source.getId(), target.getId())) {
                 continue;
             }
-            if (!seedEntityIds.contains(source.getId()) && !seedEntityIds.contains(target.getId())) {
+            if (scopeToSeedEntities
+                    && !seedEntityIds.contains(source.getId())
+                    && !seedEntityIds.contains(target.getId())) {
                 continue;
             }
             edges.add(toServiceGraphEdge(row, source.getId(), target.getId(), start, end));
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
index 05a24d50c8..cb95e56700 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
@@ -42,6 +42,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -206,8 +207,7 @@ class EntityDetailObservabilityReadModelServiceTest {
                 .thenReturn(statusPageSummary);
         when(entityResponseHandoffReadModelService.buildResponseHandoffs(
                 eq(501L), any(ObservedEntityContext.class), same(activeAlerts), same(monitors),
-                same(enrichedLogSummary), same(traceSummary), same(metricEvidence), same(logEvidence),
-                same(traceEvidence), same(traceQueryHints), same(opsSummary))).thenReturn(handoffs);
+                any(EntitySignalEvidenceBundle.class), same(opsSummary))).thenReturn(handoffs);
         when(entityNoiseControlReadModelService.buildNoiseControlSummary(
                 same(entityDto), same(monitors), same(activeAlerts), eq("team-a")))
                 .thenReturn(noiseSummary);
@@ -223,6 +223,8 @@ class EntityDetailObservabilityReadModelServiceTest {
         assertSame(monitorSummary, detail.getMonitorSummary());
         assertSame(enrichedLogSummary, detail.getLogSummary());
         assertSame(traceSummary, detail.getTraceSummary());
+        assertEquals(enrichedLogSummary, detail.getSignalEvidence().getLogSummary());
+        assertEquals(traceSummary, detail.getSignalEvidence().getTraceSummary());
         assertSame(unifiedSummary, detail.getUnifiedEvidenceSummary());
         assertSame(triage, detail.getTriageRecommendation());
         assertSame(opsSummary, detail.getOpsSummary());
@@ -235,6 +237,20 @@ class EntityDetailObservabilityReadModelServiceTest {
         assertSame(enrichedLogHints, detail.getLogQueryHints());
         assertSame(traceQueryHints, detail.getTraceQueryHints());
         assertSame(definitionActivities, detail.getDefinitionActivities());
+        verify(entityResponseHandoffReadModelService).buildResponseHandoffs(
+                eq(501L), any(ObservedEntityContext.class), same(activeAlerts), same(monitors),
+                org.mockito.ArgumentMatchers.argThat(bundle ->
+                        bundle != null
+                                && bundle.getLogSummary() == enrichedLogSummary
+                                && bundle.getTraceSummary() == traceSummary
+                                && bundle.getMetricEvidence() == metricEvidence
+                                && bundle.getLogEvidence() == logEvidence
+                                && bundle.getTraceEvidence() == traceEvidence
+                                && bundle.getLogQueryHints() == enrichedLogHints
+                                && bundle.getTraceQueryHints() == traceQueryHints
+                                && bundle.getUnifiedEvidenceSummary() == unifiedSummary
+                                && bundle.getTriageRecommendation() == triage),
+                same(opsSummary));
     }
 
     @Test
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java
index 9622b5489b..03b768864c 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java
@@ -45,6 +45,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 
 /**
@@ -188,7 +189,7 @@ class EntityTopologyQueryServiceTest {
                 .status((byte) 1)
                 .build();
 
-        lenient().when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Sort.class)))
+        lenient().when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Pageable.class)))
                 .thenReturn(List.of(checkout, payment));
         lenient().when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
                 ids != null && ids.containsAll(List.of(10L, 20L))))).thenReturn(List.of(checkout, payment));
@@ -217,7 +218,83 @@ class EntityTopologyQueryServiceTest {
                 Long.valueOf(501L).equals(edge.getRelationId())
                         && "10".equals(edge.getSourceNodeId())
                         && "monitor:701".equals(edge.getTargetNodeId())));
-        verify(entityWorkspaceAccessService).findAccessibleEntitiesForRequestWorkspace(any(Sort.class));
+        verify(entityWorkspaceAccessService).findAccessibleEntitiesForRequestWorkspace(argThat((Pageable pageable) ->
+                pageable != null
+                        && pageable.isPaged()
+                        && pageable.getPageSize() == 64
+                        && pageable.getSort().getOrderFor("gmtUpdate") != null
+                        && pageable.getSort().getOrderFor("id") != null));
+        verify(entityWorkspaceAccessService, never()).findAccessibleEntitiesForRequestWorkspace(any(Sort.class));
+    }
+
+    @Test
+    void queriesDefaultTraceOverviewWithoutSeedScopeSoLargeGraphsAreNotTruncated() {
+        List<ObserveEntity> seeds = new java.util.ArrayList<>();
+        seeds.add(entity(1L, "service", "hb-mix-1780329856-edge-gateway", "scale-mix", "prod", "unknown"));
+        for (int index = 0; index < 12; index++) {
+            seeds.add(entity(100L + index, "service",
+                    "hb-mix-1780329856-domain-%02d".formatted(index), "scale-mix", "prod", "unknown"));
+        }
+        TraceCallTopologyEdgeInfo traceEdge = new TraceCallTopologyEdgeInfo(
+                "trace-call:100:200:trace-domain-00",
+                100L,
+                200L,
+                "trace-domain-00",
+                "span-domain-00",
+                "CALL hb-mix-1780329856-svc-00-000",
+                "2026-05-20T03:01:00Z",
+                "2026-05-20T03:08:00Z",
+                "ok",
+                100,
+                new TraceCallTopologyEdgeInfo.RedMetrics(1D / 3600D, 1L, 0D, 0L, 42D, 21D)
+        );
+        ObserveEntity leaf = entity(200L, "service", "hb-mix-1780329856-svc-00-000",
+                "scale-mix", "prod", "healthy");
+
+        when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Pageable.class)))
+                .thenReturn(seeds);
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
+                ids != null && ids.contains(1L) && ids.contains(100L) && ids.contains(111L))))
+                .thenReturn(seeds);
+        when(traceCallTopologyQueryService.findTraceCallEdgesForOverview(
+                eq("prod"), eq(1710000000000L), eq(1710003600000L), eq(true))).thenReturn(
+                        new TraceCallTopologyReadModel(Map.of(200L, leaf), List.of(traceEdge)));
+
+        EntityTopologyGraphInfo graph = entityTopologyQueryService.buildFocusedTopology(
+                null, 2, "prod", "otlp-trace-call", 1710000000000L, 1710003600000L);
+
+        assertEquals(1, graph.getEdges().size());
+        assertTrue(graph.getSourceKinds().contains("otlp-trace-call"));
+        assertTrue(graph.getNodes().stream().anyMatch(node ->
+                "hb-mix-1780329856-svc-00-000".equals(node.getEntityName())));
+        verify(entityWorkspaceAccessService).findAccessibleEntitiesForRequestWorkspace(argThat((Pageable pageable) ->
+                pageable != null
+                        && pageable.isPaged()
+                        && pageable.getPageSize() == 64
+                        && pageable.getSort().getOrderFor("gmtUpdate") != null
+                        && pageable.getSort().getOrderFor("id") != null));
+        verify(traceCallTopologyQueryService, never()).findTraceCallEdges(any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void leavesSourceKindsEmptyWhenDefaultTopologyHasOnlyIsolatedNodes() {
+        ObserveEntity checkout = entity(10L, "service", "checkout-api", "commerce", "prod", "warning");
+        ObserveEntity payment = entity(20L, "service", "payment-api", "commerce", "prod", "healthy");
+
+        when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Pageable.class)))
+                .thenReturn(List.of(checkout, payment));
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
+                ids != null && ids.containsAll(List.of(10L, 20L))))).thenReturn(List.of(checkout, payment));
+        when(entityRelationQueryService.findEntityRelations(10L)).thenReturn(List.of());
+        when(entityRelationQueryService.findEntityRelations(20L)).thenReturn(List.of());
+
+        EntityTopologyGraphInfo graph = entityTopologyQueryService.buildFocusedTopology(
+                null, 1, "prod", "entity-relation");
+
+        assertEquals(List.of(), graph.getSourceKinds());
+        assertEquals(List.of(10L, 20L),
+                graph.getNodes().stream().map(EntityTopologyGraphInfo.Node::getEntityId).toList());
+        assertEquals(List.of(), graph.getEdges());
     }
 
     @Test
@@ -423,7 +500,8 @@ class EntityTopologyQueryServiceTest {
         EntityTopologyGraphInfo graph = entityTopologyQueryService.buildFocusedTopology(
                 10L, 1, "prod", "monitor-bind", 1710000000000L, 1710003600000L);
 
-        assertEquals(List.of("monitor-bind"), graph.getSourceKinds());
+        assertEquals(List.of(), graph.getSourceKinds());
+        assertEquals(List.of(), graph.getEdges());
         verify(traceCallTopologyQueryService, never()).findTraceCallEdges(any(), any(), any(), any(), any());
     }
 
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java
index ef9d723142..1161ee54b7 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java
@@ -36,6 +36,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 
 /**
@@ -199,6 +200,29 @@ class EntityWorkspaceAccessServiceTest {
         assertEquals(List.of(teamAlphaEntity), entities);
     }
 
+    @Test
+    void findAccessibleEntitiesForRequestWorkspacePreservesPageableLimit() {
+        Sort sort = Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id"));
+        PageRequest pageable = PageRequest.of(0, 12, sort);
+        ObserveEntity teamAlphaEntity = ObserveEntity.builder()
+                .id(421L)
+                .name("checkout")
+                .workspaceId("team-a")
+                .build();
+        ObserveEntity teamBetaEntity = ObserveEntity.builder()
+                .id(422L)
+                .name("billing")
+                .workspaceId("team-b")
+                .build();
+        when(entityWorkspaceQueryService.findEntities("team-a", pageable))
+                .thenReturn(List.of(teamAlphaEntity, teamBetaEntity));
+        AuthTokenRequestContext.bindWorkspaceId(" team-a ");
+
+        List<ObserveEntity> entities = workspaceAccessService.findAccessibleEntitiesForRequestWorkspace(pageable);
+
+        assertEquals(List.of(teamAlphaEntity), entities);
+    }
+
     @Test
     void findAccessibleEntityUsesCurrentRequestWorkspaceAndRejectsCrossWorkspaceRows() {
         ObserveEntity teamAlphaEntity = ObserveEntity.builder()
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java
index 0e4603f3d8..5c96967467 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java
@@ -88,6 +88,32 @@ class EntityWorkspaceQueryServiceTest {
         verify(observeEntityDao).findAll(sort);
     }
 
+    @Test
+    void findEntitiesRoutesWorkspaceAndUnscopedPagedLookup() {
+        PageRequest pageable = PageRequest.of(0, 12, Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id")));
+        ObserveEntity checkout = ObserveEntity.builder()
+                .id(411L)
+                .name("checkout")
+                .workspaceId("team-a")
+                .build();
+        ObserveEntity billing = ObserveEntity.builder()
+                .id(412L)
+                .name("billing")
+                .workspaceId("team-b")
+                .build();
+        Page<ObserveEntity> persistedPage = new PageImpl<>(List.of(checkout, billing), pageable, 2);
+        when(observeEntityDao.findAllByWorkspaceId("team-a", pageable)).thenReturn(List.of(checkout));
+        when(observeEntityDao.findAll(pageable)).thenReturn(persistedPage);
+
+        List<ObserveEntity> teamEntities = entityWorkspaceQueryService.findEntities("team-a", pageable);
+        List<ObserveEntity> allEntities = entityWorkspaceQueryService.findEntities(null, pageable);
+
+        assertEquals(List.of(checkout), teamEntities);
+        assertEquals(List.of(checkout, billing), allEntities);
+        verify(observeEntityDao).findAllByWorkspaceId("team-a", pageable);
+        verify(observeEntityDao).findAll(pageable);
+    }
+
     @Test
     void findEntityPageUsesPersistedCatalogRows() {
         PageRequest pageable = PageRequest.of(0, 8, Sort.by(Sort.Order.desc("gmtUpdate")));
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeederTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeederTest.java
new file mode 100644
index 0000000000..dc38ac840d
--- /dev/null
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeederTest.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
+import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.apache.hertzbeat.manager.dao.ObserveEntityDao;
+import org.springframework.boot.DefaultApplicationArguments;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Contract for the local demo topology relation seed that prevents node-only demo graphs.
+ */
+@ExtendWith(MockitoExtension.class)
+class LocalTopologyDemoRelationSeederTest {
+
+    @Mock
+    private ObserveEntityDao observeEntityDao;
+    @Mock
+    private EntityRelationDao entityRelationDao;
+    @Mock
+    private EntityIdentityDao entityIdentityDao;
+
+    @Test
+    void seedsCheckoutPaymentAndOrdersRelationsWhenDemoEntitiesExistWithoutEdges() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        ObserveEntity checkout = entity(10L, "service", "Checkout API");
+        ObserveEntity payment = entity(20L, "service", "Payment API");
+        ObserveEntity orders = entity(30L, "database", "Orders DB");
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Checkout API"))
+                .thenReturn(Optional.of(checkout));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Payment API"))
+                .thenReturn(Optional.of(payment));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "database", "commerce", "Orders DB"))
+                .thenReturn(Optional.of(orders));
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(10L, 10L)).thenReturn(List.of());
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(20L, 20L)).thenReturn(List.of());
+
+        seeder.seedDemoRelations();
+
+        ArgumentCaptor<List<EntityRelation>> rowsCaptor = ArgumentCaptor.forClass(List.class);
+        verify(entityRelationDao).saveAll(rowsCaptor.capture());
+        List<EntityRelation> rows = rowsCaptor.getValue();
+        assertEquals(2, rows.size());
+        EntityRelation checkoutToPayment = rows.get(0);
+        assertEquals(10L, checkoutToPayment.getSourceEntityId());
+        assertEquals(20L, checkoutToPayment.getTargetEntityId());
+        assertEquals("service:commerce/Payment API", checkoutToPayment.getTargetRef());
+        assertEquals("trace-call", checkoutToPayment.getRelationType());
+        assertEquals("otlp-trace-call", checkoutToPayment.getRelationSource());
+        assertEquals("local-topology-demo-seed", checkoutToPayment.getAttributes().get("seed"));
+        EntityRelation paymentToOrders = rows.get(1);
+        assertEquals(20L, paymentToOrders.getSourceEntityId());
+        assertEquals(30L, paymentToOrders.getTargetEntityId());
+        assertEquals("database:commerce/Orders DB", paymentToOrders.getTargetRef());
+        assertEquals("depends_on", paymentToOrders.getRelationType());
+        assertEquals("cmdb-manual-label", paymentToOrders.getRelationSource());
+    }
+
+    @Test
+    void doesNotSeedWhenRelationsAlreadyExist() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Checkout API"))
+                .thenReturn(Optional.of(entity(10L, "service", "Checkout API")));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Payment API"))
+                .thenReturn(Optional.of(entity(20L, "service", "Payment API")));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "database", "commerce", "Orders DB"))
+                .thenReturn(Optional.of(entity(30L, "database", "Orders DB")));
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(10L, 10L)).thenReturn(List.of(
+                EntityRelation.builder().sourceEntityId(10L).targetEntityId(20L).relationType("trace-call").build()));
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(20L, 20L)).thenReturn(List.of(
+                EntityRelation.builder().sourceEntityId(20L).targetEntityId(30L).relationType("depends_on").build()));
+
+        seeder.seedDemoRelations();
+
+        verify(entityRelationDao, never()).saveAll(eq(List.of()));
+        verify(entityRelationDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+    }
+
+    @Test
+    void startupDoesNotSeedLargeMixedScaleProofByDefault() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+
+        seeder.run(new DefaultApplicationArguments());
+
+        verify(observeEntityDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+        verify(entityIdentityDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+    }
+
+    @Test
+    void startupSeedsLargeMixedScaleProofOnlyWhenExplicitlyEnabled() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                anyString(), anyString(), anyString(), anyString()))
+                .thenReturn(Optional.empty());
+
+        seeder.run(new DefaultApplicationArguments("--hertzbeat.topology.local-scale-proof-seed=true"));
+
+        ArgumentCaptor<List<ObserveEntity>> entitiesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(observeEntityDao).saveAll(entitiesCaptor.capture());
+        assertEquals(1993, entitiesCaptor.getValue().size());
+    }
+
+    @Test
+    void seedsMixedScaleProofEntitiesAndServiceNameIdentitiesForEveryGreptimeService() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                eq(AuthTokenScopes.DEFAULT_WORKSPACE_ID), eq("service"), eq("scale-mix"), anyString()))
+                .thenReturn(Optional.empty());
+
+        seeder.seedMixedScaleProofEntities();
+
+        ArgumentCaptor<List<ObserveEntity>> entitiesCaptor = ArgumentCaptor.forClass(List.class);
+        ArgumentCaptor<List<EntityIdentity>> identitiesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(observeEntityDao).saveAll(entitiesCaptor.capture());
+        verify(entityIdentityDao).saveAll(identitiesCaptor.capture());
+        List<ObserveEntity> entities = entitiesCaptor.getValue();
+        List<EntityIdentity> identities = identitiesCaptor.getValue();
+        assertEquals(1993, entities.size());
+        assertEquals(1993, identities.size());
+        assertEquals("hb-mix-1780329856-edge-gateway", entities.getFirst().getName());
+        assertEquals("scale-mix", entities.getFirst().getNamespace());
+        assertEquals("prod", entities.getFirst().getEnvironment());
+        assertEquals("service", entities.getFirst().getType());
+        assertEquals("hb-mix-1780329856-edge-gateway", identities.getFirst().getIdentityValue());
+        assertEquals("service.name", identities.getFirst().getIdentityKey());
+        assertEquals("otel_resource", identities.getFirst().getIdentityType());
+        assertEquals("hb-mix-1780329856-svc-11-164", entities.getLast().getName());
+        assertEquals("hb-mix-1780329856-svc-11-164", identities.getLast().getIdentityValue());
+    }
+
+    @Test
+    void backfillsMixedScaleProofServiceNameIdentityWhenEntityAlreadyExists() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                eq(AuthTokenScopes.DEFAULT_WORKSPACE_ID), eq("service"), eq("scale-mix"), anyString()))
+                .thenAnswer(invocation -> {
+                    String serviceName = invocation.getArgument(3, String.class);
+                    return Optional.of(ObserveEntity.builder()
+                            .id(646566130000000L + Math.abs(serviceName.hashCode()))
+                            .workspaceId(AuthTokenScopes.DEFAULT_WORKSPACE_ID)
+                            .type("service")
+                            .namespace("scale-mix")
+                            .name(serviceName)
+                            .environment("prod")
+                            .build());
+                });
+        when(entityIdentityDao.findAllByEntityIdOrderByPriorityDescIdAsc(anyLong()))
+                .thenReturn(List.of());
+
+        seeder.seedMixedScaleProofEntities();
+
+        ArgumentCaptor<List<EntityIdentity>> identitiesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(observeEntityDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+        verify(entityIdentityDao).saveAll(identitiesCaptor.capture());
+        List<EntityIdentity> identities = identitiesCaptor.getValue();
+        EntityIdentity gatewayIdentity = identities.stream()
+                .filter(identity -> "hb-mix-1780329856-edge-gateway".equals(identity.getIdentityValue()))
+                .findFirst()
+                .orElseThrow();
+        assertEquals("service.name", gatewayIdentity.getIdentityKey());
+        assertEquals("hb-mix-1780329856-edge-gateway", gatewayIdentity.getIdentityValue());
+        assertEquals("hb-mix-1780329856-edge-gateway", gatewayIdentity.getNormalizedValue());
+        assertEquals(1993, identities.size());
+    }
+
+    private ObserveEntity entity(Long id, String type, String name) {
+        return ObserveEntity.builder()
+                .id(id)
+                .workspaceId(AuthTokenScopes.DEFAULT_WORKSPACE_ID)
+                .type(type)
+                .namespace("commerce")
+                .name(name)
+                .environment("prod")
+                .build();
+    }
+}
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java
index 28044ed467..4720a64659 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java
@@ -249,6 +249,43 @@ class TraceCallTopologyQueryServiceTest {
         assertEquals(1L, edge.redMetrics().requestCount());
     }
 
+    @Test
+    void returnsEmptyWhenRawTraceFallbackTimesOutAfterServiceGraphHasNoEdges() throws InterruptedException {
+        TraceCallTopologyQueryService boundedService = new TraceCallTopologyQueryService(
+                traceQueryRepository,
+                entityIdentityQueryService,
+                entityWorkspaceAccessService,
+                Duration.ofMillis(25));
+        ObserveEntity checkout = entity(10L, "checkout-api", "commerce", "prod");
+        CountDownLatch releaseRawTraceQuery = new CountDownLatch(1);
+        when(entityIdentityQueryService.findIdentities(10L)).thenReturn(List.of(
+                identity(10L, "service.name", "checkout-api")));
+        when(traceQueryRepository.queryTraceServiceGraphRows(
+                eq(1500), eq(1710000000000L), eq(1710003600000L), eq("prod"),
+                argThat(serviceNames -> serviceNames != null && serviceNames.contains("checkout-api")),
+                eq(true)))
+                .thenReturn(List.of());
+        when(traceQueryRepository.queryRecentTraceRows(1500, 1710000000000L, 1710003600000L, null, "prod", true))
+                .thenAnswer(invocation -> {
+                    releaseRawTraceQuery.await(5, TimeUnit.SECONDS);
+                    return List.of(traceRow("trace-late", "root", null, "GET /checkout",
+                            "checkout-api", "STATUS_CODE_OK", 20D));
+                });
+
+        long startedAt = System.nanoTime();
+        TraceCallTopologyReadModel readModel;
+        try {
+            readModel = boundedService.findTraceCallEdges(
+                    List.of(checkout), "prod", 1710000000000L, 1710003600000L);
+        } finally {
+            releaseRawTraceQuery.countDown();
+        }
+
+        assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt) < 1000);
+        assertTrue(readModel.entityById().isEmpty());
+        assertTrue(readModel.edges().isEmpty());
+    }
+
     @Test
     void pushesSeedServiceScopeIntoGreptimeServiceGraphAggregation() {
         ObserveEntity checkout = entity(10L, "Checkout API", "commerce", "prod");
@@ -325,6 +362,48 @@ class TraceCallTopologyQueryServiceTest {
                 1500, 1710000000000L, 1710003600000L, null, "prod", false);
     }
 
+    @Test
+    void derivesDefaultOverviewServiceGraphWithoutSeedScope() {
+        ObserveEntity gateway = entity(10L, "edge-gateway", "scale-mix", "prod");
+        ObserveEntity domain = entity(20L, "domain-00", "scale-mix", "prod");
+        ObserveEntity service = entity(30L, "svc-00-000", "scale-mix", "prod");
+        when(traceQueryRepository.queryTraceServiceGraphRows(
+                eq(1500), eq(1710000000000L), eq(1710003600000L), eq("prod"),
+                argThat(serviceNames -> serviceNames != null && serviceNames.isEmpty()),
+                eq(true)))
+                .thenReturn(List.of(
+                        serviceGraphRow("edge-gateway", "domain-00", 12L, 0L, 75D, 42D,
+                                "trace-star", "span-domain", "GET /domain",
+                                "2026-05-20T04:01:00Z", "2026-05-20T04:08:00Z"),
+                        serviceGraphRow("domain-00", "svc-00-000", 124L, 1L, 95D, 54D,
+                                "trace-mesh", "span-service", "CALL svc-00-000",
+                                "2026-05-20T04:02:00Z", "2026-05-20T04:09:00Z")));
+        when(entityIdentityQueryService.findMatchingIdentities(argThat(keys ->
+                keys != null && keys.contains("service.name")), argThat(values ->
+                values != null && values.containsAll(Set.of("edge gateway", "domain 00", "svc 00 000")))))
+                .thenReturn(List.of(
+                        identity(10L, "service.name", "edge-gateway"),
+                        identity(20L, "service.name", "domain-00"),
+                        identity(30L, "service.name", "svc-00-000")));
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
+                ids != null && ids.containsAll(Set.of(10L, 20L, 30L)))))
+                .thenReturn(List.of(gateway, domain, service));
+
+        TraceCallTopologyReadModel readModel = traceCallTopologyQueryService.findTraceCallEdgesForOverview(
+                "prod", 1710000000000L, 1710003600000L, true);
+
+        assertEquals(Set.of(10L, 20L, 30L), readModel.entityById().keySet());
+        assertEquals(2, readModel.edges().size());
+        assertEquals(10L, readModel.edges().get(0).sourceEntityId());
+        assertEquals(20L, readModel.edges().get(0).targetEntityId());
+        assertEquals(20L, readModel.edges().get(1).sourceEntityId());
+        assertEquals(30L, readModel.edges().get(1).targetEntityId());
+        assertEquals(124L, readModel.edges().get(1).redMetrics().requestCount());
+        assertEquals(1L, readModel.edges().get(1).redMetrics().errorCount());
+        verify(traceQueryRepository, never()).queryRecentTraceRows(
+                1500, 1710000000000L, 1710003600000L, null, "prod", true);
+    }
+
     @Test
     void resolvesGreptimeServiceGraphRowsByAccessibleEntityNameWhenIdentityIsMissing() {
         ObserveEntity checkout = entity(10L, "Checkout API", "commerce", "prod");
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
index 2f8daf53ff..71250b4e0b 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
@@ -192,14 +192,22 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readSeverityBuckets(Long start, Long end, String traceId, String spanId,
                                                   Integer severityNumber, String severityText, String search,
                                                   boolean hideInternal, boolean hideNoise) {
-        if (hasWorkspaceContext()) {
-            return null;
-        }
+        String normalizedWorkspaceId = hasWorkspaceContext()
+                ? AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId())
+                : null;
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
-                Map<String, Long> aggregate = historyDataReader.countLogsBySeverityBuckets(
-                        start, end, traceId, spanId, severityNumber, severityText, search,
-                        hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                Map<String, Long> aggregate;
+                if (StringUtils.hasText(normalizedWorkspaceId)) {
+                    aggregate = historyDataReader.countLogsBySeverityBuckets(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise),
+                            shouldRequireServiceName(hideInternal, hideNoise), normalizedWorkspaceId);
+                } else {
+                    aggregate = historyDataReader.countLogsBySeverityBuckets(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                }
                 if (aggregate != null && !aggregate.isEmpty()) {
                     return aggregate;
                 }
@@ -213,14 +221,22 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readTraceCoverage(Long start, Long end, String traceId, String spanId,
                                                 Integer severityNumber, String severityText, String search,
                                                 boolean hideInternal, boolean hideNoise) {
-        if (hasWorkspaceContext()) {
-            return null;
-        }
+        String normalizedWorkspaceId = hasWorkspaceContext()
+                ? AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId())
+                : null;
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
-                Map<String, Long> aggregate = historyDataReader.countLogTraceCoverage(
-                        start, end, traceId, spanId, severityNumber, severityText, search,
-                        hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                Map<String, Long> aggregate;
+                if (StringUtils.hasText(normalizedWorkspaceId)) {
+                    aggregate = historyDataReader.countLogTraceCoverage(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise),
+                            shouldRequireServiceName(hideInternal, hideNoise), normalizedWorkspaceId);
+                } else {
+                    aggregate = historyDataReader.countLogTraceCoverage(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                }
                 if (aggregate != null && !aggregate.isEmpty()) {
                     return aggregate;
                 }
@@ -234,14 +250,22 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readHourlyStats(Long start, Long end, String traceId, String spanId,
                                               Integer severityNumber, String severityText, String search,
                                               boolean hideInternal, boolean hideNoise) {
-        if (hasWorkspaceContext()) {
-            return null;
-        }
+        String normalizedWorkspaceId = hasWorkspaceContext()
+                ? AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId())
+                : null;
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
-                Map<String, Long> aggregate = historyDataReader.countLogsByHour(
-                        start, end, traceId, spanId, severityNumber, severityText, search,
-                        hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                Map<String, Long> aggregate;
+                if (StringUtils.hasText(normalizedWorkspaceId)) {
+                    aggregate = historyDataReader.countLogsByHour(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise),
+                            shouldRequireServiceName(hideInternal, hideNoise), normalizedWorkspaceId);
+                } else {
+                    aggregate = historyDataReader.countLogsByHour(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                }
                 if (aggregate != null && !aggregate.isEmpty()) {
                     return aggregate;
                 }
@@ -331,6 +355,30 @@ public class LogQueryServiceImpl implements LogQueryService {
                                                  Integer severityNumber, String severityText, String search,
                                                  PageRequest pageRequest, int offset, int pageSize,
                                                  boolean hideInternal, boolean hideNoise) {
+        String normalizedWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId());
+        Set<String> hiddenServiceNames = hiddenServiceNames(hideInternal, hideNoise);
+        boolean requireServiceName = shouldRequireServiceName(hideInternal, hideNoise);
+        for (HistoryDataReader historyDataReader : historyDataReaders) {
+            try {
+                long totalElements = historyDataReader.countLogsByMultipleConditions(
+                        start, end, traceId, spanId, severityNumber, severityText, search,
+                        hiddenServiceNames, requireServiceName, normalizedWorkspaceId);
+                if (totalElements <= 0) {
+                    continue;
+                }
+                List<LogEntry> pagedLogs = historyDataReader.queryLogsByMultipleConditionsWithPagination(
+                        start, end, traceId, spanId, severityNumber, severityText, search, offset, pageSize,
+                        hiddenServiceNames, requireServiceName, normalizedWorkspaceId);
+                List<LogEntry> guardedLogs = filterQueryLogs(pagedLogs, hideInternal, hideNoise);
+                long safeTotal = guardedLogs.size() < (pagedLogs == null ? 0 : pagedLogs.size())
+                        ? offset + guardedLogs.size()
+                        : totalElements;
+                return new PageImpl<>(guardedLogs, pageRequest, safeTotal);
+            } catch (UnsupportedOperationException ex) {
+                // Fall back to row-based workspace filtering for history stores without native workspace predicates.
+            }
+        }
+
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
                 List<LogEntry> logs;
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
index 729053e240..ac65e7546a 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
@@ -38,6 +38,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -581,20 +582,29 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa
         if (request == null) {
             return new EntityResponseHandoffsInfo();
         }
+        EntitySignalEvidenceBundle signalEvidence = request.getSignalEvidence();
+        EntityLogSummaryInfo logSummary = signalEvidence == null ? request.getLogSummary() : signalEvidence.getLogSummary();
+        EntityTraceSummaryDto traceSummary = signalEvidence == null ? request.getTraceSummary() : signalEvidence.getTraceSummary();
+        List<MetricEvidence> metricEvidence = signalEvidence == null ? request.getMetricEvidence() : signalEvidence.getMetricEvidence();
+        List<LogEvidence> logEvidence = signalEvidence == null ? request.getLogEvidence() : signalEvidence.getLogEvidence();
+        List<TraceEvidence> traceEvidence = signalEvidence == null ? request.getTraceEvidence() : signalEvidence.getTraceEvidence();
+        List<EntityTraceQueryHintDto> traceQueryHints = signalEvidence == null
+                ? request.getTraceQueryHints()
+                : signalEvidence.getTraceQueryHints();
         String alertSearchToken = buildEntityAlertSearchToken(request.getEntityContext(), request.getActiveAlerts());
-        String logSearchToken = buildEntityLogSearchToken(request.getEntityContext(), request.getLogSummary());
-        String traceSearchToken = buildEntityTraceSearchToken(request.getEntityContext(), request.getTraceSummary());
+        String logSearchToken = buildEntityLogSearchToken(request.getEntityContext(), logSummary);
+        String traceSearchToken = buildEntityTraceSearchToken(request.getEntityContext(), traceSummary);
         String fallbackSearchToken = defaultText(
                 trimToNull(request.getEntityContext() == null || request.getEntityContext().getEntity() == null
                         ? null : request.getEntityContext().getEntity().getName()),
                 trimToNull(request.getEntityContext() == null || request.getEntityContext().getEntity() == null
                         ? null : request.getEntityContext().getEntity().getDisplayName()));
         MetricEvidence preferredMetricEvidence =
-                CollectionUtils.isEmpty(request.getMetricEvidence()) ? null : request.getMetricEvidence().getFirst();
+                CollectionUtils.isEmpty(metricEvidence) ? null : metricEvidence.getFirst();
         LogEvidence preferredLogEvidence =
-                CollectionUtils.isEmpty(request.getLogEvidence()) ? null : request.getLogEvidence().getFirst();
+                CollectionUtils.isEmpty(logEvidence) ? null : logEvidence.getFirst();
         TraceEvidence preferredTraceEvidence =
-                CollectionUtils.isEmpty(request.getTraceEvidence()) ? null : request.getTraceEvidence().getFirst();
+                CollectionUtils.isEmpty(traceEvidence) ? null : traceEvidence.getFirst();
         return new EntityResponseHandoffsInfo(
                 buildEntityAlertHandoff(request.getReturnTo(), request.getReturnLabel(),
                         request.getEntityContext(), request.getActiveAlerts()),
@@ -602,9 +612,9 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa
                         request.getMonitors(), fallbackSearchToken, preferredMetricEvidence),
                 buildEntityLogHandoff(request.getReturnTo(), request.getReturnLabel(),
                         logSearchToken, request.getActiveAlerts(), preferredLogEvidence,
-                        request.getTraceSummary(), preferredTraceEvidence, request.getTraceQueryHints()),
+                        traceSummary, preferredTraceEvidence, traceQueryHints),
                 buildEntityTraceHandoff(request.getReturnTo(), request.getReturnLabel(),
-                        traceSearchToken, request.getTraceSummary(), preferredTraceEvidence),
+                        traceSearchToken, traceSummary, preferredTraceEvidence),
                 buildEntityDiscoveryHandoff(request.getReturnTo(), request.getReturnLabel(),
                         request.getEntityOwner(), request.getEntitySystem(),
                         request.getEntityEnvironment(), request.getEntitySource(),
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
index 8b23e9b6af..b9e760b621 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
@@ -100,6 +100,22 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             return new EntityTraceSummaryDto(0, 0, null, false, null);
         }
         long now = System.currentTimeMillis();
+        if (traceQueryRepository.supportsTraceSummaryRows()) {
+            Map<String, Object> row = traceQueryRepository.queryTraceSummaryRows(
+                    Math.max(0L, now - DEFAULT_LOOKBACK_MILLIS),
+                    now,
+                    preferredIdentityValue(identityValues, "service.name"),
+                    preferredIdentityValue(identityValues, "service.namespace"),
+                    preferredIdentityValue(identityValues, "deployment.environment.name"),
+                    AuthTokenRequestContext.currentWorkspaceId(),
+                    identityValues,
+                    false
+            );
+            EntityTraceSummaryDto summary = toEntityTraceSummary(row);
+            if (summary != null) {
+                return summary;
+            }
+        }
         List<TraceAggregate> traces = aggregateTraceRows(queryRecentRows(identityValues, now))
                 .stream()
                 .filter(trace -> matchesEntity(trace, identityValues))
@@ -229,6 +245,42 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
     @Override
     public TraceOverviewDto getTraceOverview(Long entityId, Long start, Long end, String traceId, Boolean errorOnly,
                                              String serviceName, String serviceNamespace, String environment, Boolean hideInternal) {
+        Map<String, Set<String>> identityValues = entityId == null ? Collections.emptyMap() : canonicalIdentityValues(loadEntityContext(entityId));
+        if (StringUtils.hasText(traceId) && traceQueryRepository.supportsTraceIdOverviewRows()) {
+            Map<String, Object> row = traceQueryRepository.queryTraceIdOverviewRows(
+                    traceId,
+                    start,
+                    end,
+                    errorOnly,
+                    serviceName,
+                    serviceNamespace,
+                    environment,
+                    AuthTokenRequestContext.currentWorkspaceId(),
+                    identityValues,
+                    hideInternal
+            );
+            TraceOverviewDto overview = toTraceOverview(row);
+            if (overview != null) {
+                return overview;
+            }
+        }
+        if (!StringUtils.hasText(traceId) && traceQueryRepository.supportsTraceOverviewRows()) {
+            Map<String, Object> row = traceQueryRepository.queryTraceOverviewRows(
+                    start,
+                    end,
+                    errorOnly,
+                    serviceName,
+                    serviceNamespace,
+                    environment,
+                    AuthTokenRequestContext.currentWorkspaceId(),
+                    identityValues,
+                    hideInternal
+            );
+            TraceOverviewDto overview = toTraceOverview(row);
+            if (overview != null) {
+                return overview;
+            }
+        }
         Page<TraceListItemDto> result = queryTraceList(entityId, start, end, traceId, errorOnly, serviceName, serviceNamespace, environment, 0,
                 TRACE_LIST_SAMPLE_LIMIT, hideInternal);
         Long latestObservedAt = result.getContent().stream()
@@ -241,6 +293,42 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         return new TraceOverviewDto((int) result.getTotalElements(), errorTraceCount, latestObservedAt, active);
     }
 
+    private TraceOverviewDto toTraceOverview(Map<String, Object> row) {
+        if (CollectionUtils.isEmpty(row)) {
+            return null;
+        }
+        int totalTraceCount = Optional.ofNullable(readIntValue(row, "total_trace_count", "totalTraceCount"))
+                .orElse(0);
+        int errorTraceCount = Optional.ofNullable(readIntValue(row, "error_trace_count", "errorTraceCount"))
+                .orElse(0);
+        Long latestObservedAt = readTimestamp(row, "latest_observed_at");
+        if (latestObservedAt == null) {
+            latestObservedAt = readTimestamp(row, "latestObservedAt");
+        }
+        boolean active = latestObservedAt != null
+                && latestObservedAt >= System.currentTimeMillis() - ACTIVE_TRACE_WINDOW_MILLIS;
+        return new TraceOverviewDto(totalTraceCount, errorTraceCount, latestObservedAt, active);
+    }
+
+    private EntityTraceSummaryDto toEntityTraceSummary(Map<String, Object> row) {
+        if (CollectionUtils.isEmpty(row)) {
+            return null;
+        }
+        int totalTraceCount = Optional.ofNullable(readIntValue(row, "total_trace_count", "totalTraceCount"))
+                .orElse(0);
+        int errorTraceCount = Optional.ofNullable(readIntValue(row, "error_trace_count", "errorTraceCount"))
+                .orElse(0);
+        Long latestObservedAt = readTimestamp(row, "latest_observed_at");
+        if (latestObservedAt == null) {
+            latestObservedAt = readTimestamp(row, "latestObservedAt");
+        }
+        String latestTraceId = defaultText(readTextValue(row, "latest_trace_id"),
+                readTextValue(row, "latestTraceId"));
+        boolean active = latestObservedAt != null
+                && latestObservedAt >= System.currentTimeMillis() - ACTIVE_TRACE_WINDOW_MILLIS;
+        return new EntityTraceSummaryDto(totalTraceCount, errorTraceCount, latestObservedAt, active, latestTraceId);
+    }
+
     private ObservedEntityContext loadEntityContext(Long entityId) {
         if (entityId == null || entityId <= 0) {
             return null;
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
index 93cb25bdb0..bf93fbfa2e 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
@@ -278,6 +278,41 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testListLogsPushesWorkspacePaginationIntoStorageWhenSupported() throws Exception {
+        LogEntry teamAlphaLog = LogEntry.builder()
+                .timeUnixNano(1734005477630000000L)
+                .severityText("INFO")
+                .body("team-a checkout log")
+                .resource(new HashMap<>(java.util.Map.of(
+                        "service.name", "checkout",
+                        "hertzbeat.workspace_id", "team-a")))
+                .build();
+
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogsByMultipleConditions(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"))).thenReturn(41L);
+        when(historyDataReader.queryLogsByMultipleConditionsWithPagination(any(), any(), any(), any(), any(), any(),
+                any(), eq(40), eq(20), anySet(), eq(false), eq("team-a")))
+                .thenReturn(List.of(teamAlphaLog));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/list")
+                        .param("pageIndex", "2")
+                        .param("pageSize", "20"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.content.length()").value(1))
+                .andExpect(jsonPath("$.data.totalElements").value(41))
+                .andExpect(jsonPath("$.data.content[0].body").value("team-a checkout log"));
+
+        verify(historyDataReader).countLogsByMultipleConditions(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader).queryLogsByMultipleConditionsWithPagination(any(), any(), any(), any(), any(), any(),
+                any(), eq(40), eq(20), anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
     @Test
     void testListLogsDoesNotLeakUnscopedTotalWhenWorkspacePageAlreadyMatches() throws Exception {
         LogEntry teamAlphaLog = LogEntry.builder()
@@ -304,6 +339,33 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testOverviewStatsPushesWorkspaceAggregateIntoStorageWhenSupported() throws Exception {
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogsBySeverityBuckets(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"))).thenReturn(java.util.Map.of(
+                        "totalCount", 3L,
+                        "infoCount", 2L,
+                        "errorCount", 1L,
+                        "fatalCount", 0L,
+                        "warnCount", 0L,
+                        "debugCount", 0L,
+                        "traceCount", 0L
+                ));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/overview"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.totalCount").value(3))
+                .andExpect(jsonPath("$.data.infoCount").value(2))
+                .andExpect(jsonPath("$.data.errorCount").value(1));
+
+        verify(historyDataReader).countLogsBySeverityBuckets(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
     @Test
     void testOverviewStatsUsesRequestWorkspaceContext() throws Exception {
         List<LogEntry> mockLogs = Arrays.asList(
@@ -523,6 +585,30 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testTraceCoverageStatsPushesWorkspaceAggregateIntoStorageWhenSupported() throws Exception {
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogTraceCoverage(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"))).thenReturn(java.util.Map.of(
+                        "withTrace", 5L,
+                        "withoutTrace", 2L,
+                        "withSpan", 4L,
+                        "withBothTraceAndSpan", 3L
+                ));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/trace-coverage"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.traceCoverage.withTrace").value(5))
+                .andExpect(jsonPath("$.data.traceCoverage.withoutTrace").value(2))
+                .andExpect(jsonPath("$.data.traceCoverage.withBothTraceAndSpan").value(3));
+
+        verify(historyDataReader).countLogTraceCoverage(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
     @Test
     void testTrendStats() throws Exception {
         // Create logs with timestamps that fall into different hours
@@ -580,4 +666,22 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testTrendStatsPushesWorkspaceAggregateIntoStorageWhenSupported() throws Exception {
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogsByHour(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a")))
+                .thenReturn(java.util.Map.of("2026-04-29 21:00", 12L));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/trend"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.hourlyStats['2026-04-29 21:00']").value(12));
+
+        verify(historyDataReader).countLogsByHour(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
 }
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
index a002e44fa8..9fbb86ddb3 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
@@ -41,6 +41,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -732,4 +733,57 @@ class EntityObservabilityGatewayImplTest {
         assertEquals("checkout-system", handoffs.getDiscovery().getSystem());
         assertEquals("ownership", handoffs.getEditor().getFocus());
     }
+
+    @Test
+    void buildEntityResponseHandoffsShouldPreferSharedSignalEvidenceBundle() {
+        ObserveEntity entity = new ObserveEntity();
+        entity.setName("checkout");
+        EntityIdentity identity = EntityIdentity.builder()
+                .identityKey("service.name")
+                .identityValue("checkout")
+                .build();
+        ObservedEntityContext entityContext = ObservedEntityContext.from(entity, List.of(identity));
+        EntityTraceSummaryDto staleTraceSummary = new EntityTraceSummaryDto(1, 0, 10L, true, "stale-trace");
+        EntityTraceSummaryDto bundleTraceSummary = new EntityTraceSummaryDto(2, 1, 20L, true, "bundle-trace");
+        EntitySignalEvidenceBundle signalEvidence = new EntitySignalEvidenceBundle(
+                new EntityLogSummaryInfo(1, "bundle", "resource", Map.of(), List.of("bundle-log"), "bundle-log"),
+                bundleTraceSummary,
+                List.of(),
+                List.of(),
+                List.of(),
+                List.of(),
+                List.of(),
+                null,
+                null
+        );
+        EntityResponseHandoffsRequest request = new EntityResponseHandoffsRequest(
+                "/entities/1",
+                "结账服务",
+                "team-a",
+                "checkout-system",
+                "prod",
+                "manual",
+                entityContext,
+                List.of(),
+                List.of(),
+                new EntityLogSummaryInfo(1, "stale", "resource", Map.of(), List.of("stale-log"), "stale-log"),
+                staleTraceSummary,
+                List.of(),
+                List.of(),
+                List.of(),
+                List.of(),
+                signalEvidence,
+                true,
+                true,
+                true,
+                true
+        );
+
+        EntityResponseHandoffsInfo handoffs = gateway.buildEntityResponseHandoffs(request);
+
+        assertEquals("bundle-trace", handoffs.getLogs().getTraceId());
+        assertEquals("bundle-trace", handoffs.getLogs().getSearch());
+        assertEquals("bundle-trace", handoffs.getTraces().getTraceId());
+        assertEquals("bundle-trace", handoffs.getTraces().getSearch());
+    }
 }
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
index 13716a30d2..397ee565c1 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
@@ -116,6 +116,51 @@ class EntityTraceQueryServiceImplTest {
         assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
     }
 
+    @Test
+    void buildEntityTraceSummaryUsesGreptimeAggregateWhenRepositorySupportsIt() {
+        long now = System.currentTimeMillis();
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        ObservedEntityContext entityContext = ObservedEntityContext.from(
+                ObserveEntity.builder().id(1L).type("service").name("checkout-service").build(),
+                List.of(
+                        identity(1L, "service.name", "checkout-service", 100, true),
+                        identity(1L, "service.namespace", "commerce", 80, false),
+                        identity(1L, "host.name", "checkout-1", 50, false)
+                ));
+        when(traceQueryRepository.supportsTraceSummaryRows()).thenReturn(true);
+        when(traceQueryRepository.queryTraceSummaryRows(
+                org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                eq("checkout-service"), eq("commerce"), org.mockito.ArgumentMatchers.isNull(),
+                eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), eq(false)))
+                .thenReturn(Map.of(
+                        "total_trace_count", 7L,
+                        "error_trace_count", 2L,
+                        "latest_observed_at", now - 30_000,
+                        "latest_trace_id", "trace-latest"
+                ));
+
+        EntityTraceSummaryDto summary = entityTraceQueryService.buildEntityTraceSummary(entityContext);
+
+        assertEquals(7, summary.getRecentTraceCount());
+        assertEquals(2, summary.getRecentErrorTraceCount());
+        assertEquals("trace-latest", summary.getLatestTraceId());
+        assertEquals(now - 30_000, summary.getLatestObservedAt());
+        assertTrue(summary.isActive());
+        ArgumentCaptor<Map<String, Set<String>>> identityFilterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryTraceSummaryRows(
+                org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                eq("checkout-service"), eq("commerce"), org.mockito.ArgumentMatchers.isNull(),
+                eq("team-a"), identityFilterCaptor.capture(), eq(false));
+        assertEquals(Set.of("checkout-service"), identityFilterCaptor.getValue().get("service.name"));
+        assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
+        assertEquals(Set.of("checkout-1"), identityFilterCaptor.getValue().get("host.name"));
+        verify(traceQueryRepository, never()).queryRecentTraceRows(
+                eq(1500), org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), org.mockito.ArgumentMatchers.any());
+    }
+
     @Test
     void queryTraceListAndDetailRespectEntityBinding() {
         long now = System.currentTimeMillis();
@@ -408,6 +453,98 @@ class EntityTraceQueryServiceImplTest {
                 eq("prod"), eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), eq(true));
     }
 
+    @Test
+    void getTraceOverviewUsesGreptimeAggregateWhenRepositorySupportsIt() {
+        long now = System.currentTimeMillis();
+        long start = now - 120_000;
+        long end = now;
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        ObserveEntity entity = ObserveEntity.builder().id(1L).type("service").name("checkout-service").build();
+        when(workspaceQueryGateway.findEntityById(1L)).thenReturn(Optional.of(entity));
+        when(workspaceQueryGateway.findIdentitiesByEntityId(1L)).thenReturn(List.of(
+                identity(1L, "service.name", "checkout-service", 100, true),
+                identity(1L, "service.namespace", "commerce", 80, false)
+        ));
+        when(traceQueryRepository.supportsTraceOverviewRows()).thenReturn(true);
+        when(traceQueryRepository.queryTraceOverviewRows(
+                eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                eq(true))).thenReturn(Map.of(
+                "total_trace_count", 41L,
+                "error_trace_count", 41L,
+                "latest_observed_at", now - 30_000
+        ));
+
+        TraceOverviewDto overview = entityTraceQueryService.getTraceOverview(1L, start, end, null,
+                true, "checkout-service", "commerce", "prod", true);
+
+        assertEquals(41, overview.getTotalTraceCount());
+        assertEquals(41, overview.getErrorTraceCount());
+        assertEquals(now - 30_000, overview.getLatestObservedAt());
+        assertTrue(overview.isHasActiveTrace());
+        ArgumentCaptor<Map<String, Set<String>>> identityFilterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryTraceOverviewRows(
+                eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), identityFilterCaptor.capture(), eq(true));
+        assertEquals(Set.of("checkout-service"), identityFilterCaptor.getValue().get("service.name"));
+        assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
+        verify(traceQueryRepository, never()).queryTraceListRows(
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyInt(),
+                org.mockito.ArgumentMatchers.anyInt());
+        verify(traceQueryRepository, never()).queryRecentTraceRows(
+                eq(1500), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), org.mockito.ArgumentMatchers.any());
+    }
+
+    @Test
+    void getTraceOverviewUsesGreptimeTraceIdAggregateWhenRepositorySupportsIt() {
+        long now = System.currentTimeMillis();
+        long start = now - 120_000;
+        long end = now;
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        ObserveEntity entity = ObserveEntity.builder().id(1L).type("service").name("checkout-service").build();
+        when(workspaceQueryGateway.findEntityById(1L)).thenReturn(Optional.of(entity));
+        when(workspaceQueryGateway.findIdentitiesByEntityId(1L)).thenReturn(List.of(
+                identity(1L, "service.name", "checkout-service", 100, true),
+                identity(1L, "service.namespace", "commerce", 80, false)
+        ));
+        when(traceQueryRepository.supportsTraceIdOverviewRows()).thenReturn(true);
+        when(traceQueryRepository.queryTraceIdOverviewRows(
+                eq("trace-filtered"), eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                eq(true))).thenReturn(Map.of(
+                "total_trace_count", 1L,
+                "error_trace_count", 1L,
+                "latest_observed_at", now - 30_000
+        ));
+
+        TraceOverviewDto overview = entityTraceQueryService.getTraceOverview(1L, start, end, "trace-filtered",
+                true, "checkout-service", "commerce", "prod", true);
+
+        assertEquals(1, overview.getTotalTraceCount());
+        assertEquals(1, overview.getErrorTraceCount());
+        assertEquals(now - 30_000, overview.getLatestObservedAt());
+        assertTrue(overview.isHasActiveTrace());
+        ArgumentCaptor<Map<String, Set<String>>> identityFilterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryTraceIdOverviewRows(
+                eq("trace-filtered"), eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), identityFilterCaptor.capture(), eq(true));
+        assertEquals(Set.of("checkout-service"), identityFilterCaptor.getValue().get("service.name"));
+        assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
+        verify(traceQueryRepository, never()).queryTraceRows(
+                eq("trace-filtered"), eq(5000), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                org.mockito.ArgumentMatchers.any());
+    }
+
     @Test
     void queryTraceListPushesTraceIdFiltersToRepository() {
         long now = System.currentTimeMillis();
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java
index 52a9a0f89c..72cb6d3713 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java
@@ -227,6 +227,195 @@ public class GreptimeTraceQueryRepository implements TraceQueryRepository {
         return queryRows(sql);
     }
 
+    @Override
+    public boolean supportsTraceOverviewRows() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsTraceIdOverviewRows() {
+        return true;
+    }
+
+    @Override
+    public Map<String, Object> queryTraceOverviewRows(Long start,
+                                                      Long end,
+                                                      Boolean errorOnly,
+                                                      String serviceName,
+                                                      String serviceNamespace,
+                                                      String environment,
+                                                      String workspaceId,
+                                                      Map<String, Set<String>> resourceIdentityFilters,
+                                                      Boolean hideInternal) {
+        String errorExpression = "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END)";
+        StringBuilder innerSql = new StringBuilder("SELECT trace_id, ")
+                .append("MIN(timestamp) AS trace_start_time, ")
+                .append(errorExpression)
+                .append(" AS error_span_count FROM ")
+                .append(TRACE_TABLE);
+        List<String> filters = new LinkedList<>();
+        if (start != null) {
+            filters.add("timestamp >= to_timestamp_millis(" + start + ")");
+        }
+        if (end != null) {
+            filters.add("timestamp <= to_timestamp_millis(" + end + ")");
+        }
+        if (StringUtils.hasText(serviceName)) {
+            filters.add("service_name = '" + escapeSql(serviceName) + "'");
+        }
+        if (StringUtils.hasText(serviceNamespace)) {
+            filters.add(resourceAttributeFilter(null, "service.namespace", serviceNamespace));
+        }
+        if (StringUtils.hasText(environment) && !"all".equalsIgnoreCase(environment.trim())) {
+            String filter = environmentFilter(null, environment);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (StringUtils.hasText(workspaceId)) {
+            String filter = workspaceFilter(null, workspaceId);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (!CollectionUtils.isEmpty(resourceIdentityFilters)) {
+            addResourceIdentityFilters(filters, null, resourceIdentityFilters, serviceName, serviceNamespace);
+        }
+        if (Boolean.TRUE.equals(hideInternal)) {
+            filters.add(SELF_TELEMETRY_SERVICE_FILTER);
+        }
+        if (!filters.isEmpty()) {
+            innerSql.append(" WHERE ").append(String.join(" AND ", filters));
+        }
+        innerSql.append(" GROUP BY trace_id");
+        if (Boolean.TRUE.equals(errorOnly)) {
+            innerSql.append(" HAVING ").append(errorExpression).append(" > 0");
+        }
+        String sql = "SELECT COUNT(*) AS total_trace_count, "
+                + "SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count, "
+                + "MAX(trace_start_time) AS latest_observed_at FROM ("
+                + innerSql
+                + ") trace_overview";
+        List<Map<String, Object>> rows = queryRows(sql);
+        return rows.isEmpty() ? Map.of() : rows.getFirst();
+    }
+
+    @Override
+    public Map<String, Object> queryTraceIdOverviewRows(String traceId,
+                                                        Long start,
+                                                        Long end,
+                                                        Boolean errorOnly,
+                                                        String serviceName,
+                                                        String serviceNamespace,
+                                                        String environment,
+                                                        String workspaceId,
+                                                        Map<String, Set<String>> resourceIdentityFilters,
+                                                        Boolean hideInternal) {
+        String errorExpression = "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END)";
+        StringBuilder innerSql = buildTraceGroupedSql(start, end, serviceName, serviceNamespace, environment,
+                workspaceId, resourceIdentityFilters, hideInternal, errorExpression, traceId);
+        if (Boolean.TRUE.equals(errorOnly)) {
+            innerSql.append(" HAVING ").append(errorExpression).append(" > 0");
+        }
+        String sql = "SELECT COUNT(*) AS total_trace_count, "
+                + "SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count, "
+                + "MAX(trace_start_time) AS latest_observed_at FROM ("
+                + innerSql
+                + ") trace_id_overview";
+        List<Map<String, Object>> rows = queryRows(sql);
+        return rows.isEmpty() ? Map.of() : rows.getFirst();
+    }
+
+    @Override
+    public boolean supportsTraceSummaryRows() {
+        return true;
+    }
+
+    @Override
+    public Map<String, Object> queryTraceSummaryRows(Long start,
+                                                     Long end,
+                                                     String serviceName,
+                                                     String serviceNamespace,
+                                                     String environment,
+                                                     String workspaceId,
+                                                     Map<String, Set<String>> resourceIdentityFilters,
+                                                     Boolean hideInternal) {
+        String errorExpression = "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END)";
+        String groupedSql = buildTraceGroupedSql(start, end, serviceName, serviceNamespace, environment,
+                workspaceId, resourceIdentityFilters, hideInternal, errorExpression, null).toString();
+        String sql = "SELECT summary.total_trace_count, summary.error_trace_count, "
+                + "latest.trace_start_time AS latest_observed_at, latest.trace_id AS latest_trace_id "
+                + "FROM (SELECT COUNT(*) AS total_trace_count, "
+                + "SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count "
+                + "FROM ("
+                + groupedSql
+                + ") entity_trace_summary) summary "
+                + "LEFT JOIN (SELECT trace_id, trace_start_time FROM ("
+                + groupedSql
+                + ") entity_trace_latest ORDER BY trace_start_time DESC LIMIT 1) latest ON TRUE";
+        List<Map<String, Object>> rows = queryRows(sql);
+        return rows.isEmpty() ? Map.of() : rows.getFirst();
+    }
+
+    private StringBuilder buildTraceGroupedSql(Long start,
+                                               Long end,
+                                               String serviceName,
+                                               String serviceNamespace,
+                                               String environment,
+                                               String workspaceId,
+                                               Map<String, Set<String>> resourceIdentityFilters,
+                                               Boolean hideInternal,
+                                               String errorExpression,
+                                               String traceId) {
+        StringBuilder innerSql = new StringBuilder("SELECT trace_id, ")
+                .append("MIN(timestamp) AS trace_start_time, ")
+                .append(errorExpression)
+                .append(" AS error_span_count FROM ")
+                .append(TRACE_TABLE);
+        List<String> filters = new LinkedList<>();
+        if (StringUtils.hasText(traceId)) {
+            filters.add("trace_id = '" + escapeSql(traceId) + "'");
+        }
+        if (start != null) {
+            filters.add("timestamp >= to_timestamp_millis(" + start + ")");
+        }
+        if (end != null) {
+            filters.add("timestamp <= to_timestamp_millis(" + end + ")");
+        }
+        if (StringUtils.hasText(serviceName)) {
+            filters.add("service_name = '" + escapeSql(serviceName) + "'");
+        }
+        if (StringUtils.hasText(serviceNamespace)) {
+            filters.add(resourceAttributeFilter(null, "service.namespace", serviceNamespace));
+        }
+        if (StringUtils.hasText(environment) && !"all".equalsIgnoreCase(environment.trim())) {
+            String filter = environmentFilter(null, environment);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (StringUtils.hasText(workspaceId)) {
+            String filter = workspaceFilter(null, workspaceId);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (!CollectionUtils.isEmpty(resourceIdentityFilters)) {
+            addResourceIdentityFilters(filters, null, resourceIdentityFilters, serviceName, serviceNamespace);
+        }
+        if (Boolean.TRUE.equals(hideInternal)) {
+            filters.add(SELF_TELEMETRY_SERVICE_FILTER);
+        }
+        if (!filters.isEmpty()) {
+            innerSql.append(" WHERE ").append(String.join(" AND ", filters));
+        }
+        innerSql.append(" GROUP BY trace_id");
+        return innerSql;
+    }
+
     @Override
     public List<Map<String, Object>> queryTraceServiceGraphRows(int limit,
                                                                 Long start,
@@ -275,15 +464,21 @@ public class GreptimeTraceQueryRepository implements TraceQueryRepository {
         filters.add("LOWER(child.service_name) != LOWER(parent.service_name)");
         if (start != null) {
             filters.add("child.timestamp >= to_timestamp_millis(" + start + ")");
+            filters.add("parent.timestamp >= to_timestamp_millis(" + start + ")");
         }
         if (end != null) {
             filters.add("child.timestamp <= to_timestamp_millis(" + end + ")");
+            filters.add("parent.timestamp <= to_timestamp_millis(" + end + ")");
         }
         if (StringUtils.hasText(environment) && !"all".equalsIgnoreCase(environment.trim())) {
             String filter = environmentFilter("child", environment);
             if (StringUtils.hasText(filter)) {
                 filters.add(filter);
             }
+            String parentFilter = environmentFilter("parent", environment);
+            if (StringUtils.hasText(parentFilter)) {
+                filters.add(parentFilter);
+            }
         }
         String serviceScopeFilter = serviceGraphServiceScopeFilter(serviceNames);
         if (StringUtils.hasText(serviceScopeFilter)) {
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java
index 553f8af85a..001639b44d 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java
@@ -134,6 +134,111 @@ public interface TraceQueryRepository {
         return List.of();
     }
 
+    /**
+     * Whether this repository can aggregate trace overview statistics in storage.
+     *
+     * @return true when storage-owned trace overview rows are supported
+     */
+    default boolean supportsTraceOverviewRows() {
+        return false;
+    }
+
+    /**
+     * Query trace overview statistics grouped in the storage layer.
+     *
+     * @param start start time in epoch milliseconds
+     * @param end end time in epoch milliseconds
+     * @param errorOnly whether only error traces should be counted
+     * @param serviceName service name filter
+     * @param serviceNamespace service namespace filter
+     * @param environment deployment environment filter
+     * @param workspaceId workspace id filter
+     * @param resourceIdentityFilters canonical resource identity filters
+     * @param hideInternal whether internal HertzBeat traces should be hidden
+     * @return row with total_trace_count, error_trace_count, and latest_observed_at
+     */
+    default Map<String, Object> queryTraceOverviewRows(Long start,
+                                                       Long end,
+                                                       Boolean errorOnly,
+                                                       String serviceName,
+                                                       String serviceNamespace,
+                                                       String environment,
+                                                       String workspaceId,
+                                                       Map<String, Set<String>> resourceIdentityFilters,
+                                                       Boolean hideInternal) {
+        return Map.of();
+    }
+
+    /**
+     * Whether this repository can aggregate a single trace-id overview in storage.
+     *
+     * @return true when storage-owned trace-id overview rows are supported
+     */
+    default boolean supportsTraceIdOverviewRows() {
+        return false;
+    }
+
+    /**
+     * Query trace overview statistics for a single trace id in the storage layer.
+     *
+     * @param traceId trace id
+     * @param start start time in epoch milliseconds
+     * @param end end time in epoch milliseconds
+     * @param errorOnly whether only error traces should be counted
+     * @param serviceName service name filter
+     * @param serviceNamespace service namespace filter
+     * @param environment deployment environment filter
+     * @param workspaceId workspace id filter
+     * @param resourceIdentityFilters canonical resource identity filters
+     * @param hideInternal whether internal HertzBeat traces should be hidden
+     * @return row with total_trace_count, error_trace_count, and latest_observed_at
+     */
+    default Map<String, Object> queryTraceIdOverviewRows(String traceId,
+                                                         Long start,
+                                                         Long end,
+                                                         Boolean errorOnly,
+                                                         String serviceName,
+                                                         String serviceNamespace,
+                                                         String environment,
+                                                         String workspaceId,
+                                                         Map<String, Set<String>> resourceIdentityFilters,
+                                                         Boolean hideInternal) {
+        return Map.of();
+    }
+
+    /**
+     * Whether this repository can aggregate entity trace summaries in storage.
+     *
+     * @return true when storage-owned entity trace summary rows are supported
+     */
+    default boolean supportsTraceSummaryRows() {
+        return false;
+    }
+
+    /**
+     * Query entity trace summary statistics in the storage layer.
+     *
+     * @param start start time in epoch milliseconds
+     * @param end end time in epoch milliseconds
+     * @param serviceName service name filter
+     * @param serviceNamespace service namespace filter
+     * @param environment deployment environment filter
+     * @param workspaceId workspace id filter
+     * @param resourceIdentityFilters canonical resource identity filters
+     * @param hideInternal whether internal HertzBeat traces should be hidden
+     * @return row with total_trace_count, error_trace_count, latest_observed_at, and latest_trace_id
+     */
+    default Map<String, Object> queryTraceSummaryRows(Long start,
+                                                      Long end,
+                                                      String serviceName,
+                                                      String serviceNamespace,
+                                                      String environment,
+                                                      String workspaceId,
+                                                      Map<String, Set<String>> resourceIdentityFilters,
+                                                      Boolean hideInternal) {
+        return Map.of();
+    }
+
     /**
      * Query service-to-service trace call graph rows with RED metrics aggregated by the storage layer.
      *
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
index 8946c2a377..9dd4d73fe4 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
@@ -174,6 +174,22 @@ public interface HistoryDataReader {
                 severityText, searchContent, offset, limit);
     }
 
+    /**
+     * Query logs with pagination, workspace scope, and storage-side noise filters when the backend supports them.
+     *
+     * @param workspaceId normalized workspace id that should own the returned logs
+     * @return filtered log entries with pagination
+     */
+    default List<LogEntry> queryLogsByMultipleConditionsWithPagination(Long startTime, Long endTime, String traceId,
+                                                                       String spanId, Integer severityNumber,
+                                                                       String severityText, String searchContent,
+                                                                       Integer offset, Integer limit,
+                                                                       Set<String> excludedServiceNames,
+                                                                       boolean requireServiceName,
+                                                                       String workspaceId) {
+        throw new UnsupportedOperationException("query workspace logs with pagination is not supported");
+    }
+
     /**
      * Count logs with multiple filter conditions (Legacy)
      */
@@ -216,6 +232,21 @@ public interface HistoryDataReader {
                 severityText, searchContent);
     }
 
+    /**
+     * Count logs with workspace scope and storage-side noise filters when the backend supports them.
+     *
+     * @param workspaceId normalized workspace id that should own the counted logs
+     * @return count of matching log entries
+     */
+    default long countLogsByMultipleConditions(Long startTime, Long endTime, String traceId,
+                                               String spanId, Integer severityNumber,
+                                               String severityText, String searchContent,
+                                               Set<String> excludedServiceNames,
+                                               boolean requireServiceName,
+                                               String workspaceId) {
+        throw new UnsupportedOperationException("count workspace logs is not supported");
+    }
+
     /**
      * Aggregate log severity buckets in the storage engine when supported.
      *
@@ -229,6 +260,21 @@ public interface HistoryDataReader {
         throw new UnsupportedOperationException("count log severity buckets is not supported");
     }
 
+    /**
+     * Aggregate log severity buckets with workspace scope in the storage engine when supported.
+     *
+     * @param workspaceId normalized workspace id that should own the aggregated logs
+     * @return a map with totalCount, fatalCount, errorCount, warnCount, infoCount, debugCount, traceCount
+     */
+    default Map<String, Long> countLogsBySeverityBuckets(Long startTime, Long endTime, String traceId,
+                                                         String spanId, Integer severityNumber,
+                                                         String severityText, String searchContent,
+                                                         Set<String> excludedServiceNames,
+                                                         boolean requireServiceName,
+                                                         String workspaceId) {
+        throw new UnsupportedOperationException("count workspace log severity buckets is not supported");
+    }
+
     /**
      * Aggregate log trace coverage in the storage engine when supported.
      *
@@ -242,6 +288,21 @@ public interface HistoryDataReader {
         throw new UnsupportedOperationException("count log trace coverage is not supported");
     }
 
+    /**
+     * Aggregate log trace coverage with workspace scope in the storage engine when supported.
+     *
+     * @param workspaceId normalized workspace id that should own the aggregated logs
+     * @return a map with withTrace, withoutTrace, withSpan, withBothTraceAndSpan
+     */
+    default Map<String, Long> countLogTraceCoverage(Long startTime, Long endTime, String traceId,
+                                                    String spanId, Integer severityNumber,
+                                                    String severityText, String searchContent,
+                                                    Set<String> excludedServiceNames,
+                                                    boolean requireServiceName,
+                                                    String workspaceId) {
+        throw new UnsupportedOperationException("count workspace log trace coverage is not supported");
+    }
+
     /**
      * Aggregate log counts by hour in the storage engine when supported.
      *
@@ -254,4 +315,19 @@ public interface HistoryDataReader {
                                              boolean requireServiceName) {
         throw new UnsupportedOperationException("count logs by hour is not supported");
     }
+
+    /**
+     * Aggregate log counts by hour with workspace scope in the storage engine when supported.
+     *
+     * @param workspaceId normalized workspace id that should own the aggregated logs
+     * @return a map keyed by yyyy-MM-dd HH:00
+     */
+    default Map<String, Long> countLogsByHour(Long startTime, Long endTime, String traceId,
+                                             String spanId, Integer severityNumber,
+                                             String severityText, String searchContent,
+                                             Set<String> excludedServiceNames,
+                                             boolean requireServiceName,
+                                             String workspaceId) {
+        throw new UnsupportedOperationException("count workspace logs by hour is not supported");
+    }
 }
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
index 594a2aea1a..86d6cc60ae 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
@@ -661,11 +661,23 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                                                       Integer offset, Integer limit,
                                                                       Set<String> excludedServiceNames,
                                                                       boolean requireServiceName) {
+        return queryLogsByMultipleConditionsWithPagination(startTime, endTime, traceId, spanId, severityNumber,
+                severityText, searchContent, offset, limit, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public List<LogEntry> queryLogsByMultipleConditionsWithPagination(Long startTime, Long endTime, String traceId,
+                                                                      String spanId, Integer severityNumber,
+                                                                      String severityText, String searchContent,
+                                                                      Integer offset, Integer limit,
+                                                                      Set<String> excludedServiceNames,
+                                                                      boolean requireServiceName,
+                                                                      String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT ").append(NATIVE_LOG_SELECT_COLUMNS)
                     .append(" FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             sql.append(" ORDER BY timestamp DESC");
 
             // Add pagination
@@ -698,10 +710,21 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                              String severityText, String searchContent,
                                              Set<String> excludedServiceNames,
                                              boolean requireServiceName) {
+        return countLogsByMultipleConditions(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public long countLogsByMultipleConditions(Long startTime, Long endTime, String traceId,
+                                             String spanId, Integer severityNumber,
+                                             String severityText, String searchContent,
+                                             Set<String> excludedServiceNames,
+                                             boolean requireServiceName,
+                                             String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT COUNT(*) as count FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
 
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows != null && !rows.isEmpty()) {
@@ -723,6 +746,17 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                                         String severityText, String searchContent,
                                                         Set<String> excludedServiceNames,
                                                         boolean requireServiceName) {
+        return countLogsBySeverityBuckets(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public Map<String, Long> countLogsBySeverityBuckets(Long startTime, Long endTime, String traceId,
+                                                        String spanId, Integer severityNumber,
+                                                        String severityText, String searchContent,
+                                                        Set<String> excludedServiceNames,
+                                                        boolean requireServiceName,
+                                                        String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT ")
                     .append("COUNT(*) as totalCount, ")
@@ -734,7 +768,7 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                     .append("SUM(CASE WHEN severity_number >= 1 AND severity_number <= 4 THEN 1 ELSE 0 END) as traceCount ")
                     .append("FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows == null || rows.isEmpty()) {
                 return Map.of();
@@ -761,6 +795,17 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                                    String severityText, String searchContent,
                                                    Set<String> excludedServiceNames,
                                                    boolean requireServiceName) {
+        return countLogTraceCoverage(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public Map<String, Long> countLogTraceCoverage(Long startTime, Long endTime, String traceId,
+                                                   String spanId, Integer severityNumber,
+                                                   String severityText, String searchContent,
+                                                   Set<String> excludedServiceNames,
+                                                   boolean requireServiceName,
+                                                   String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT ")
                     .append("COUNT(*) as totalCount, ")
@@ -770,7 +815,7 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                     .append("AND span_id IS NOT NULL AND span_id != '' THEN 1 ELSE 0 END) as withBothTraceAndSpan ")
                     .append("FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows == null || rows.isEmpty()) {
                 return Map.of();
@@ -796,11 +841,22 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                              String severityText, String searchContent,
                                              Set<String> excludedServiceNames,
                                              boolean requireServiceName) {
+        return countLogsByHour(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public Map<String, Long> countLogsByHour(Long startTime, Long endTime, String traceId,
+                                             String spanId, Integer severityNumber,
+                                             String severityText, String searchContent,
+                                             Set<String> excludedServiceNames,
+                                             boolean requireServiceName,
+                                             String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT date_bin('1 hour', timestamp) as hour, ")
                     .append("COUNT(*) as count FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             sql.append(" GROUP BY hour ORDER BY hour ASC");
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows == null || rows.isEmpty()) {
@@ -891,6 +947,14 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
     private void buildWhereConditions(StringBuilder sql, Long startTime, Long endTime, String traceId,
                                      String spanId, Integer severityNumber, String severityText, String searchContent,
                                      Set<String> excludedServiceNames, boolean requireServiceName) {
+        buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    private void buildWhereConditions(StringBuilder sql, Long startTime, Long endTime, String traceId,
+                                     String spanId, Integer severityNumber, String severityText, String searchContent,
+                                     Set<String> excludedServiceNames, boolean requireServiceName,
+                                     String workspaceId) {
         List<String> conditions = new ArrayList<>();
 
         if (startTime != null && endTime != null) {
@@ -939,12 +1003,34 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
             }
         }
 
+        String workspaceCondition = workspaceCondition(workspaceId);
+        if (StringUtils.hasText(workspaceCondition)) {
+            conditions.add(workspaceCondition);
+        }
+
         // Add WHERE clause if there are conditions
         if (!conditions.isEmpty()) {
             sql.append(" WHERE ").append(String.join(" AND ", conditions));
         }
     }
 
+    private String workspaceCondition(String workspaceId) {
+        if (!StringUtils.hasText(workspaceId)) {
+            return null;
+        }
+        String normalizedWorkspaceId = safeString(workspaceId.trim());
+        String hertzbeatWorkspace = "json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]')";
+        String workspace = "json_get_string(resource_attributes, '$[\"workspace.id\"]')";
+        List<String> predicates = new ArrayList<>();
+        predicates.add(hertzbeatWorkspace + " = '" + normalizedWorkspaceId + "'");
+        predicates.add(workspace + " = '" + normalizedWorkspaceId + "'");
+        if ("default".equals(normalizedWorkspaceId)) {
+            predicates.add("((" + hertzbeatWorkspace + " IS NULL OR " + hertzbeatWorkspace + " = '')"
+                    + " AND (" + workspace + " IS NULL OR " + workspace + " = ''))");
+        }
+        return "(" + String.join(" OR ", predicates) + ")";
+    }
+
     private List<LogEntry> mapRowsToLogEntries(List<Map<String, Object>> rows) {
         List<LogEntry> list = new LinkedList<>();
         if (rows == null || rows.isEmpty()) {
diff --git a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java
index ca798a4fb1..c0bd4b756e 100644
--- a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java
+++ b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java
@@ -198,6 +198,8 @@ class GreptimeTraceQueryRepositoryTest {
         assertTrue(sql.contains("FROM (SELECT trace_id"));
         assertTrue(sql.contains("SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
                 + "THEN 1 ELSE 0 END) AS error_span_count"));
+        assertEquals(1, sql.split(" AS resource_attributes", -1).length - 1,
+                "trace-list SQL must project resource_attributes with exactly one alias");
         assertTrue(sql.contains("FROM hzb_traces WHERE timestamp >= to_timestamp_millis(1710000000000)"));
         assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
         assertTrue(sql.contains("service_name = 'checkout'"));
@@ -213,6 +215,143 @@ class GreptimeTraceQueryRepositoryTest {
         assertTrue(sql.endsWith("ORDER BY timestamp DESC LIMIT 20 OFFSET 40"));
     }
 
+    @Test
+    void queryTraceOverviewRowsPushesAggregateFiltersIntoGreptimeSql() {
+        when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
+        when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of(
+                "total_trace_count", 42L,
+                "error_trace_count", 7L,
+                "latest_observed_at", 1710003600000L)));
+
+        Map<String, Object> overview = repository.queryTraceOverviewRows(
+                1710000000000L,
+                1710003600000L,
+                true,
+                "checkout",
+                "commerce",
+                "prod",
+                "team-a",
+                Map.of("host.name", Set.of("checkout-1", "checkout-2")),
+                true);
+
+        assertEquals(42L, overview.get("total_trace_count"));
+        ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+        verify(greptimeSqlQueryExecutor, times(2)).execute(sqlCaptor.capture());
+        String sql = sqlCaptor.getValue();
+        assertTrue(sql.startsWith("SELECT COUNT(*) AS total_trace_count"));
+        assertTrue(sql.contains("SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count"));
+        assertTrue(sql.contains("MAX(trace_start_time) AS latest_observed_at"));
+        assertTrue(sql.contains("FROM (SELECT trace_id, MIN(timestamp) AS trace_start_time"));
+        assertTrue(sql.contains("SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END) AS error_span_count"));
+        assertTrue(sql.contains("FROM hzb_traces WHERE timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("service_name = 'checkout'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"service.namespace\"]') = 'commerce'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a' "
+                + "OR json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a')"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-1' "
+                + "OR json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-2')"));
+        assertTrue(sql.contains("LOWER(service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
+        assertTrue(sql.contains("GROUP BY trace_id HAVING "
+                + "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') THEN 1 ELSE 0 END) > 0"));
+        assertTrue(sql.endsWith(") trace_overview"));
+    }
+
+    @Test
+    void queryTraceIdOverviewRowsPushesTraceIdAggregateFiltersIntoGreptimeSql() {
+        when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
+        when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of(
+                "total_trace_count", 1L,
+                "error_trace_count", 1L,
+                "latest_observed_at", 1710003600000L)));
+
+        Map<String, Object> overview = repository.queryTraceIdOverviewRows(
+                "trace-'filtered",
+                1710000000000L,
+                1710003600000L,
+                true,
+                "checkout",
+                "commerce",
+                "prod",
+                "team-a",
+                Map.of("host.name", Set.of("checkout-1", "checkout-2")),
+                true);
+
+        assertEquals(1L, overview.get("total_trace_count"));
+        ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+        verify(greptimeSqlQueryExecutor, times(2)).execute(sqlCaptor.capture());
+        String sql = sqlCaptor.getValue();
+        assertTrue(sql.startsWith("SELECT COUNT(*) AS total_trace_count"));
+        assertTrue(sql.contains("SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count"));
+        assertTrue(sql.contains("MAX(trace_start_time) AS latest_observed_at"));
+        assertTrue(sql.contains("FROM (SELECT trace_id, MIN(timestamp) AS trace_start_time"));
+        assertTrue(sql.contains("trace_id = 'trace-''filtered'"));
+        assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("service_name = 'checkout'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"service.namespace\"]') = 'commerce'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a' "
+                + "OR json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a')"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-1' "
+                + "OR json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-2')"));
+        assertTrue(sql.contains("LOWER(service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
+        assertTrue(sql.contains("GROUP BY trace_id HAVING "
+                + "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') THEN 1 ELSE 0 END) > 0"));
+        assertTrue(sql.endsWith(") trace_id_overview"));
+    }
+
+    @Test
+    void queryTraceSummaryRowsPushesAggregateAndLatestTraceIntoGreptimeSql() {
+        when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
+        when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of(
+                "total_trace_count", 7L,
+                "error_trace_count", 2L,
+                "latest_observed_at", 1710003600000L,
+                "latest_trace_id", "trace-latest")));
+
+        Map<String, Object> summary = repository.queryTraceSummaryRows(
+                1710000000000L,
+                1710003600000L,
+                "checkout",
+                "commerce",
+                "prod",
+                "team-a",
+                Map.of("host.name", Set.of("checkout-1", "checkout-2")),
+                true);
+
+        assertEquals("trace-latest", summary.get("latest_trace_id"));
+        ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+        verify(greptimeSqlQueryExecutor, times(2)).execute(sqlCaptor.capture());
+        String sql = sqlCaptor.getValue();
+        assertTrue(sql.startsWith("SELECT summary.total_trace_count"));
+        assertTrue(sql.contains("summary.error_trace_count"));
+        assertTrue(sql.contains("latest.trace_start_time AS latest_observed_at"));
+        assertTrue(sql.contains("latest.trace_id AS latest_trace_id"));
+        assertTrue(sql.contains("SELECT COUNT(*) AS total_trace_count"));
+        assertTrue(sql.contains("SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count"));
+        assertTrue(sql.contains("FROM (SELECT trace_id, MIN(timestamp) AS trace_start_time"));
+        assertTrue(sql.contains("SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END) AS error_span_count"));
+        assertTrue(sql.contains("FROM hzb_traces WHERE timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("service_name = 'checkout'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"service.namespace\"]') = 'commerce'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a' "
+                + "OR json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a')"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-1' "
+                + "OR json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-2')"));
+        assertTrue(sql.contains("LOWER(service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
+        assertTrue(sql.contains("ORDER BY trace_start_time DESC LIMIT 1"));
+        assertTrue(sql.endsWith(") latest ON TRUE"));
+    }
+
     @Test
     void queryTraceServiceGraphRowsPushesServiceGraphRedAggregationIntoGreptimeSql() {
         when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
@@ -249,8 +388,12 @@ class GreptimeTraceQueryRepositoryTest {
         assertTrue(sql.contains("child.parent_span_id = parent.span_id"));
         assertTrue(sql.contains("child.timestamp >= to_timestamp_millis(1710000000000)"));
         assertTrue(sql.contains("child.timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("parent.timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("parent.timestamp <= to_timestamp_millis(1710003600000)"));
         assertTrue(sql.contains("json_get_string(child.resource_attributes, '$[\"deployment.environment.name\"]') "
                 + "= 'prod'"));
+        assertTrue(sql.contains("json_get_string(parent.resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
         assertTrue(sql.contains("((child.service_name = 'checkout-api' OR child.service_name = 'payment-api') "
                 + "OR (parent.service_name = 'checkout-api' OR parent.service_name = 'payment-api'))"));
         assertTrue(sql.contains("LOWER(child.service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
diff --git a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
index c20c019747..21ca023e5f 100644
--- a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
+++ b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
@@ -352,6 +352,51 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testQueryLogsCanPushDownWorkspaceScopeWithPagination() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(createNativeLogRows());
+
+            greptimeDbDataStorage.queryLogsByMultipleConditionsWithPagination(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout", 40, 20,
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertTrue(sql.contains("ORDER BY timestamp DESC LIMIT 20 OFFSET 40"));
+        }
+    }
+
+    @Test
+    void testCountLogsCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of("count", 7L)));
+
+            long count = greptimeDbDataStorage.countLogsByMultipleConditions(
+                    1710000000000L, 1710000060000L, null, null, null, null, null,
+                    Set.of(), false, "team-a");
+
+            assertEquals(7L, count);
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("SELECT COUNT(*) as count FROM hertzbeat_logs"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+        }
+    }
+
     @Test
     void testLogStatsUseGreptimeAggregates() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
@@ -383,6 +428,42 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testLogStatsCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString()))
+                    .thenReturn(List.of(Map.of(
+                            "totalcount", 9L,
+                            "fatalcount", 0L,
+                            "errorcount", 1L,
+                            "warncount", 2L,
+                            "infocount", 6L,
+                            "debugcount", 0L,
+                            "tracecount", 0L)));
+
+            Map<String, Long> result = greptimeDbDataStorage.countLogsBySeverityBuckets(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout",
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            assertEquals(9L, result.get("totalCount"));
+            assertEquals(1L, result.get("errorCount"));
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("COUNT(*) as totalCount"));
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("service_name IS NOT NULL"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertFalse(sql.contains("ORDER BY"));
+        }
+    }
+
     @Test
     void testLogTraceCoverageUsesGreptimeAggregates() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
@@ -410,6 +491,38 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testLogTraceCoverageCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString()))
+                    .thenReturn(List.of(Map.of(
+                            "totalcount", 12L,
+                            "withtrace", 7L,
+                            "withspan", 6L,
+                            "withbothtraceandspan", 5L)));
+
+            Map<String, Long> result = greptimeDbDataStorage.countLogTraceCoverage(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout",
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            assertEquals(7L, result.get("withTrace"));
+            assertEquals(5L, result.get("withoutTrace"));
+            assertEquals(5L, result.get("withBothTraceAndSpan"));
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertFalse(sql.contains("ORDER BY timestamp DESC"));
+        }
+    }
+
     @Test
     void testLogTrendUsesGreptimeHourAggregate() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
@@ -431,6 +544,34 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testLogTrendCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString()))
+                    .thenReturn(List.of(Map.of("hour", 1777467600000000000L, "count", 12L)));
+
+            Map<String, Long> result = greptimeDbDataStorage.countLogsByHour(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout",
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            assertEquals(12L, result.values().iterator().next());
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("date_bin('1 hour', timestamp) as hour"));
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertTrue(sql.contains("GROUP BY hour ORDER BY hour ASC"));
+            assertFalse(sql.contains("SELECT timestamp, trace_id"));
+        }
+    }
+
     @Test
     void testQueryLogsWithPagination() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index fc92222fca..99e0ce9978 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -231,15 +231,23 @@ describe('topology page', () => {
 
   it('keeps topology on the HertzBeat entity relationship surface instead of copied service-map chrome', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
+    const controllerSource = readFileSync(resolve(process.cwd(), 'lib/topology-surface/controller.ts'), 'utf8');
 
     expect(source).toContain('loadTopologyGraph');
     expect(source).toContain('resolveTopologyApiTimeoutMs');
-    expect(source).toContain('const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;');
-    expect(source).toContain('const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;');
-    expect(source).toContain('return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;');
-    expect(source).toContain('return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(controllerSource).toContain('TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;');
+    expect(controllerSource).toContain('CMDB_TOPOLOGY_API_TIMEOUT_MS = 60000;');
+    expect(controllerSource).toContain('DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;');
+    expect(controllerSource).toContain('return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(controllerSource).toContain('return CMDB_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(controllerSource).toContain('return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;');
     expect(source).not.toContain('return 8000');
-    expect(source).toContain('{ timeoutMs: resolveTopologyApiTimeoutMs(routeContext) }');
+    expect(source).toContain('const [topologyManualRefreshSequence, setTopologyManualRefreshSequence]');
+    expect(source).toContain('const handleTopologyRefresh = React.useCallback');
+    expect(source).toContain('cacheBust: () => `manual-${topologyManualRefreshSequence}`');
+    expect(source).toContain('onClick: handleTopologyRefresh');
+    expect(source).toContain("'data-topology-refresh-action-behavior': 'in-page-api-reload'");
+    expect(source).toContain('timeoutMs: resolveTopologyApiTimeoutMs(routeContext)');
     expect(source).toContain('buildTopologyServiceMapFromApiGraph');
     expect(source).toContain('HzTopologyWorkbenchFrame');
     expect(source).toContain('HzTopologyWorkbenchHeader');
@@ -248,8 +256,14 @@ describe('topology page', () => {
     expect(source).toContain('HzTopologyCanvas');
     expect(source).toContain('HzTopologyCanvasAnnotation');
     expect(source).toContain('HzTopologyG6Canvas');
+    expect(source).toContain("from '@hertzbeat/ui/topology'");
+    expect(source).not.toContain("from '@hertzbeat/ui'");
     expect(source).toContain("from '@hertzbeat/ui/topology-g6'");
     expect(source).toContain("selectedFocusLabel={t('topology.view.focus-selected')}");
+    expect(source).toContain("edgeDensityDrilldownLabel={t('topology.edge-density.open-table')}");
+    expect(source).toContain('edgeDensityDrilldownTargetId="topology-metric-table"');
+    expect(source).toContain("nodeOnlyExplanationLabel={t('topology.node-only.explanation')}");
+    expect(readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8')).toContain("'topology.node-only.explanation'");
     expect(source).toContain('HzTopologyFocusTrail');
     expect(source).toContain('HzTopologyScopeBar');
     expect(source).toContain('HzTopologyToolbar');
@@ -276,10 +290,12 @@ describe('topology page', () => {
     expect(source).toContain('copyVisibility="assistive"');
     expect(source).not.toContain('data-topology-header-density="concise-operational"');
     expect(source).toContain('data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
-    expect(source).toContain('layout="canvas-only"');
+    expect(source).toContain("const topologyWorkbenchLayout = topologyShouldShowCompanionRail ? 'canvas-companion' : 'canvas-only';");
+    expect(source).toContain('layout={topologyWorkbenchLayout}');
     expect(source).toContain('data-topology-canvas-slot-owner="hertzbeat-ui-workbench-slot"');
     expect(source).toContain('data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"');
-    expect(source).toContain('data-topology-companion-rail-visibility="hidden-from-layout"');
+    expect(source).toContain("const topologyCompanionRailVisibility = topologyShouldShowCompanionRail ? 'visible-side-rail' : 'hidden-from-layout';");
+    expect(source).toContain('data-topology-companion-rail-visibility={topologyCompanionRailVisibility}');
     expect(source).toContain('kind="canvas"');
     expect(source).toContain('kind="companion"');
     expect(source).toContain('surface="content"');
@@ -334,7 +350,7 @@ describe('topology page', () => {
     expect(source).toContain('const map = React.useMemo(');
     expect(source).toContain('buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t)');
     expect(source).toContain('const topologyG6Graph = React.useMemo<HzTopologyG6GraphInput>(() => ({');
-    expect(source).toContain('focus: node.focus');
+    expect(source).toContain("focus: topologyCanShowApiFocus ? node.focus : 'normal'");
     expect(source).not.toContain('topologyCanvasFocusNodeId');
     expect(source).toContain('topologyG6Graph');
     expect(source).toContain('graph={topologyG6Graph}');
@@ -418,6 +434,46 @@ describe('topology page', () => {
     expect(source).not.toContain('Select Environment/s');
   });
 
+  it('keeps the route entry lightweight so topology page cold compilation can stream a shell first', () => {
+    const routeSource = readFileSync(resolve(process.cwd(), 'app/topology/page.tsx'), 'utf8');
+    const clientEntrySource = readFileSync(resolve(process.cwd(), 'app/topology/topology-route-client.tsx'), 'utf8');
+
+    expect(routeSource).toContain("import TopologyRouteClient from './topology-route-client'");
+    expect(routeSource).toContain("import TopologyRouteShell from './topology-route-shell'");
+    expect(routeSource).not.toContain("import TopologyPage from './topology-page'");
+    expect(routeSource).toContain('<TopologyRouteShell />');
+    expect(routeSource).toContain('<TopologyRouteClient routeContext={routeContext} shellElementId="topology-route-deferred-shell" />');
+    expect(clientEntrySource).toContain("dynamic(() => import('./topology-page')");
+    expect(clientEntrySource).toContain('ssr: false');
+    expect(clientEntrySource).toContain("from '@hertzbeat/ui/topology-g6-runtime'");
+    expect(clientEntrySource).toContain('void preloadHzTopologyG6Runtime();');
+    expect(clientEntrySource).toContain('preloadTopologyGraph(routeContext ?? {},');
+    expect(clientEntrySource).toContain('if (routeContext?.scaleProof?.trim()) return;');
+    expect(clientEntrySource).toContain('data-topology-route-client-scale-proof-prefetch="skipped"');
+    expect(clientEntrySource).toContain("data-topology-route-client-prefetch=\"topology-api-before-heavy-page\"");
+    expect(clientEntrySource).toContain("document.getElementById(shellElementId)?.setAttribute('hidden', '')");
+    expect(readFileSync(resolve(process.cwd(), 'app/topology/topology-route-shell.tsx'), 'utf8')).toContain(
+      'data-topology-route-shell="deferred-client-entry"'
+    );
+  });
+
+  it('keeps the optional G6 browser smoke configurable for alpha/local runs', () => {
+    const source = readFileSync(resolve(process.cwd(), 'scripts/topology-g6-browser-smoke.spec.ts'), 'utf8');
+
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_BASE_URL');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_USERNAME');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_PASSWORD');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_ROUTE');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_EXPECTED_NODES');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_EXPECTED_EDGES');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_FOCUS_NODE_ID');
+    expect(source).toContain('test.skip(!hasSmokeConfig');
+    expect(source).not.toContain("identifier: 'admin'");
+    expect(source).not.toContain("credential: 'hertzbeat'");
+    expect(source).not.toContain('entityId=642126742338816');
+    expect(source).not.toContain('Payment+API');
+  });
+
   it('keeps G6 single-click node and edge selection inside the topology page drawer', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
 
@@ -430,10 +486,10 @@ describe('topology page', () => {
     expect(source).toContain("setTopologyLocalSelection({ nodeId, edgeId: undefined, source: 'node-click' })");
     expect(source).toContain("setTopologyLocalSelection({ nodeId: undefined, edgeId, source: 'edge-click' })");
     expect(source).toContain("setTopologyLocalSelection({ nodeId: undefined, edgeId: row.id, source: 'table-row-click' })");
-    expect(source).toContain('const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map);');
+    expect(source).toContain('const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map, topologyCanvasEdges);');
     expect(source).toContain('const topologyCanvasSelectedEdgeId = topologyLocalSelection.nodeId');
     expect(source).toContain("? undefined");
-    expect(source).toContain(': topologyLocalSelectedEdge?.id ?? map.selectedEdgeId ?? topologyDetailEdge?.id;');
+    expect(source).toContain(': topologyLocalSelectedEdge?.id ?? map.selectedEdgeId;');
     expect(source).toContain('const topologyMetricSelectedRowId = topologyLocalSelection.nodeId');
     expect(source).toContain('selectedRowId={topologyMetricSelectedRowId}');
     expect(source).toContain('data-topology-selection-behavior="in-page-drawer"');
@@ -452,12 +508,16 @@ describe('topology page', () => {
     expect(source).toContain('data-topology-focus-route-preservation-owner="hertzbeat-ui-g6-focus-entry"');
     expect(source).toContain('data-topology-focus-route-preservation-source="loaded-api-graph-state"');
     expect(source).toContain('buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t)');
+    expect(source).toContain('shouldPreservePreviousTopologyGraphDuringLoad(routeContext ?? {})');
+    expect(source).toContain('const topologyShouldPreservePreviousGraphDuringLoad =');
+    expect(source).toContain('topologyManualRefreshSequence === 0 && shouldPreservePreviousTopologyGraphDuringLoad(routeContext ?? {})');
+    expect(source).toContain('if (!topologyShouldPreservePreviousGraphDuringLoad)');
+    expect(source).toContain('setLoadedApiGraph(undefined);');
     expect(source).toContain('navigateTopologyFocus(topologyNodeFocusHrefById.get(nodeId), topologyRouter.push);');
     expect(source).toContain('const handleTopologyFocusExit = React.useCallback');
     expect(source).toContain('event.preventDefault();');
     expect(source).toContain('navigateTopologyFocus(topologyFocusExitHref, topologyRouter.push);');
     expect(source).toContain('onClick: handleTopologyFocusExit');
-    expect(source).not.toContain('setLoadedApiGraph(undefined);');
     expect(source).not.toContain('window.location.href = href;');
     expect(source).not.toContain('const topologyNodeHrefById');
     expect(source).not.toContain('[topologyNodeHrefById]');
@@ -466,6 +526,18 @@ describe('topology page', () => {
     expect(source).not.toContain('navigateTopologySelection(topologyEdgeHrefById.get(row.id));');
   });
 
+  it('keeps selected path summary content ahead of hover-only preview content', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
+
+    expect(source).toContain('const topologySelectedPathSummaryEdge = topologyCanvasSelectedEdgeId');
+    expect(source).toContain('const topologyInvestigationEdge = topologySelectedPathSummaryEdge ?? topologyHoveredDetailEdge ?? topologyDetailEdge;');
+    expect(source).not.toContain('const topologyInvestigationEdge = topologyHoveredDetailEdge ?? topologyDetailEdge;');
+    expect(source).toContain('const topologyPathSummaryInteractionState = topologyCanvasSelectedEdgeId');
+    expect(source).toContain("? 'selected'");
+    expect(source).toContain(': topologyG6HoveredEdgeId');
+    expect(source).toContain("? 'hovered'");
+  });
+
   it('routes toolbar scope controls through soft navigation without clearing the G6 graph', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
 
@@ -474,7 +546,8 @@ describe('topology page', () => {
     expect(source).toContain('const topologyG6Layout = topologyLayout ===');
     expect(source).not.toContain('const handleTopologyLayoutChange = React.useCallback');
     expect(source).not.toContain('setTopologyLayout(value as TopologyLayoutMode);');
-    expect(source).toContain('const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(undefined);');
+    expect(source).toContain('const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(routeContext?.search);');
+    expect(source).toContain('setTopologySearchQuery(routeContext?.search);');
     expect(source).toContain('const topologyEffectiveSearchQuery = topologySearchQuery ?? map.filterContext.search;');
     expect(source).toContain("const topologyToolbarSearchQuery = topologySearchQuery ?? '';");
     expect(source).toContain('const topologyG6SearchQuery = topologyToolbarSearchQuery;');
@@ -496,7 +569,7 @@ describe('topology page', () => {
     expect(source).toContain('const handleTopologyGroupByChange = React.useCallback');
     expect(source).toContain("buildTopologyScopeHref(topologyG6ResetParams, { groupBy: value })");
     expect(source).toMatch(
-      /const topologyG6ResetParams = React\.useMemo\(\(\) => \{[\s\S]*if \(map\.filterContext\.groupBy !== 'none'\) params\.set\('groupBy', map\.filterContext\.groupBy\);/
+      /const topologyG6ResetParams = React\.useMemo\(\(\) => \{[\s\S]*if \(topologyHasRenderableGraph && map\.filterContext\.groupBy !== 'none'\) params\.set\('groupBy', map\.filterContext\.groupBy\);/
     );
     expect(source).toContain('const handleTopologyResetScope = React.useCallback');
     expect(source).toContain('event.preventDefault();');
@@ -517,7 +590,7 @@ describe('topology page', () => {
     expect(source).toContain("layout={topologyG6Layout}");
     expect(source).toContain('searchQuery={topologyG6SearchQuery}');
     expect(source).toContain("if (topologyToolbarSearchQuery) topologyG6SearchParams.set('search', topologyToolbarSearchQuery);");
-    expect(source).not.toContain('setLoadedApiGraph(undefined);');
+    expect(source).toContain('if (!topologyShouldPreservePreviousGraphDuringLoad)');
   });
 
   it('exposes initial in-page selection source markers for real topology browser checks', async () => {
@@ -528,6 +601,29 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-selection-source="none"');
     expect(html).toContain('data-topology-selection-node-id="none"');
     expect(html).toContain('data-topology-selection-edge-id="none"');
+    expect(html).toContain('data-topology-live-interaction-proof="wheel-hover-click-preserve-url-viewport"');
+    expect(html).toContain('data-topology-live-interaction-proof-owner="hertzbeat-ui-g6-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-live-interaction-owner="hertzbeat-ui-g6-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-live-interaction-invariants="no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-owner="hertzbeat-ui-g6-edge-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-invariants="edge-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-topology-normal-browser-giant-node-proof="compact-1x-no-fill"');
+    expect(html).toContain('data-topology-normal-browser-giant-node-proof-owner="hertzbeat-ui-g6-viewport"');
+    expect(html).toContain('data-topology-ordinary-inspection-proof="api-current-window-no-stale-empty-gap"');
+    expect(html).toContain('data-topology-ordinary-inspection-proof-owner="hertzbeat-ui-workbench-frame"');
+    expect(html).toContain('data-hz-topology-g6-focused-edge="none"');
+    expect(html).toContain('data-hz-topology-g6-selected-node-rendered=');
+    expect(html).toContain('data-hz-topology-g6-selected-edge-rendered=');
+    expect(html).toContain('data-hz-topology-g6-selected-edge-count="0"');
+    expect(html).toContain('data-topology-path-summary="selected-edge-context"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="none"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="none"');
+    expect(html).toContain('data-hz-topology-path-selected-edge="none"');
+    expect(html).toContain('data-hz-topology-path-hovered-edge="none"');
+    expect(html).toContain('路径预览');
+    expect(html).not.toMatch(/data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>选中路径</);
     expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression="selection-preserve-viewport"');
     expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression-owner="hertzbeat-ui-g6-mount-lifecycle"');
     expect(html).toContain('data-topology-g6-mount-lifecycle-selection-policy="node-edge-table-drawer-only"');
@@ -554,6 +650,7 @@ describe('topology page', () => {
 
   it('renders HertzBeat topology sources, graph nodes, and operations closure actions', async () => {
     const { default: TopologyPage } = await import('./topology-page');
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
     const html = renderToStaticMarkup(<TopologyPage apiGraph={buildApiTopologyFixture()} />);
 
     expect(html).toContain('data-topology-route="hertzbeat-entity-topology"');
@@ -583,7 +680,8 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
     expect(html).toContain('data-hz-ui="topology-workbench-grid"');
     expect(html).toContain('data-hz-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
-    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-only"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
     expect(html).toContain('data-topology-scope-bar-owner="hertzbeat-ui-scope-bar"');
     expect(html).toContain('data-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"');
     expect(html).toContain('data-hz-ui="topology-scope-bar"');
@@ -642,8 +740,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-focus-filter="time-range"');
     expect(html).toContain('data-hz-topology-focus-filter="source"');
     expect(html).toContain('data-hz-topology-focus-filter="view"');
-    expect(html).toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
-    expect(html).toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('当前范围隐藏 0 个');
     expect(html).toContain('data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit"');
     expect(html).toContain('data-topology-focus-trail-exit-owner="hertzbeat-ui-focus-trail-exit"');
     expect(html).toMatch(
@@ -669,8 +768,14 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-primitive="path-summary"');
     expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
     expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
-    expect(html).toContain('data-hz-topology-path-selected-edge="svc-frontend--svc-checkout"');
-    expect(html).toContain('data-hz-topology-path-hovered-edge="svc-frontend--svc-checkout"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="none"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="none"');
+    expect(html).toContain('data-hz-topology-path-selected-edge="none"');
+    expect(html).toContain('data-hz-topology-path-hovered-edge="none"');
+    expect(html).toContain('路径预览');
+    expect(html).not.toMatch(/data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>选中路径</);
     expect(html).toContain('data-hz-topology-path-source-id="svc-frontend"');
     expect(html).toContain('data-hz-topology-path-target-id="svc-checkout"');
     expect(html).toContain('data-hz-topology-path-relation-type="trace-call"');
@@ -698,10 +803,13 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
     expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
     expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
+    expect(html).toContain('data-hz-topology-toolbar-control-flow="single-grid-row"');
+    expect(html).toContain('data-hz-topology-toolbar-empty-offset="none"');
+    expect(html).toContain('[grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]');
     expect(html).not.toContain('overflow-x-auto px-4 py-1');
     expect(html).toContain('overflow-x-auto px-0 py-1');
-    expect(html).toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
-    expect(html).not.toContain('h-7 !pl-2 !pr-1.5');
+    expect(html).toContain('h-7 !gap-1.5 !px-2');
+    expect(html).not.toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight-owner="hertzbeat-ui-toolbar-visual-weight"');
     expect(html).toContain('data-hz-topology-toolbar-secondary-visibility="assistive"');
@@ -723,6 +831,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-toolbar-control-strip="source-depth-group-reset"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-display="contents"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
     expect(html).toContain('data-hz-topology-control="depth"');
     expect(html).toContain('data-hz-topology-control-depth-owner="hertzbeat-ui-toolbar-depth-control"');
@@ -767,7 +876,7 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-companion-rail-owner="hertzbeat-ui-companion-rail"');
     expect(html).toContain('data-topology-companion-rail-boundary-owner="hertzbeat-ui-companion-rail-boundary"');
     expect(html).toContain('data-topology-companion-rail-scope="legend-metrics-timeline-detail"');
-    expect(html).toContain('data-topology-companion-rail-visibility="hidden-from-layout"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
     expect(html).toContain('data-topology-companion-rail-priority="graph-first"');
     expect(html).toContain('data-hz-ui="topology-companion-rail"');
     expect(html).toContain('data-hz-topology-primitive="companion-rail"');
@@ -791,6 +900,15 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-companion-jump-list-scroll-scope-owner="hertzbeat-ui-companion-jump-list-scroll-scope"');
     expect(html).toContain('data-hz-topology-companion-jump-list-active-mode="contained-rail-scroll"');
     expect(html).toContain('data-hz-topology-companion-jump-list-active-mode-owner="hertzbeat-ui-companion-jump-list-active-mode"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-sync="manual-active-resets-scroll-active"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-sync-owner="hertzbeat-ui-companion-jump-list-selection-sync"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-scroll="active-section"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-scroll-owner="hertzbeat-ui-companion-jump-list-selection-scroll"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-url-policy="replace-active-section-hash"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-url-policy-owner="hertzbeat-ui-companion-jump-list-selection-url-policy"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-active-reset-key=');
+    expect(source).toContain("const topologyCompanionSelectionResetKey = topologyLocalSelection.source === 'none'");
+    expect(source).toContain('activeResetKey={topologyCompanionSelectionResetKey}');
     expect(html).toContain('data-hz-topology-companion-jump-item="view-mode"');
     expect(html).toContain('data-hz-topology-companion-jump-href="#topology-companion-view-mode"');
     expect(html).toContain('data-hz-topology-companion-jump-scroll-owner="hertzbeat-ui-companion-jump-scroll"');
@@ -831,7 +949,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-workbench-slot-kind="canvas"');
     expect(html).toContain('data-hz-topology-workbench-slot-kind="companion"');
     expect(html).toContain('data-hz-topology-workbench-slot-surface="content"');
-    expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness="none"');
+    expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness="sticky-with-companion"');
     expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness-owner="hertzbeat-ui-workbench-grid-canvas-stickiness"');
     expect(html).toContain('data-hz-ui="topology-canvas"');
     expect(html).toContain('data-hz-topology-primitive="canvas"');
@@ -880,12 +998,15 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-command-event="hz-topology-g6-viewport-command"');
     expect(html).toContain('data-hz-topology-g6-command-request="idle"');
     expect(html).toContain('data-hz-topology-g6-command-request-owner="hertzbeat-ui-g6-command-request"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard="drag-pan-suppresses-click-selection"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard-owner="hertzbeat-ui-g6-pan-selection-guard"');
     expect(html).toContain('data-hz-topology-g6-wheel-mode="manual-clamped-g6-zoom"');
     expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
     expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
     expect(html).toContain('data-hz-topology-g6-wheel-origin="pointer-clamped"');
-    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-2.2"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-1.35"');
     expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-tier="compact-readable"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts="plus-minus-zero-fit"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts-owner="hertzbeat-ui-g6-keyboard"');
     expect(html).toContain('data-hz-topology-g6-keyboard-actions="zoom-in zoom-out reset-view fit-view"');
@@ -1000,6 +1121,8 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-legend-owner="hertzbeat-ui-legend"');
     expect(html).toContain('data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).toContain('data-hz-topology-g6-legend-dock="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-state="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-container="true"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-placement="canvas-bottom-left"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-occlusion="inside-canvas-low-interruption"');
@@ -1014,44 +1137,41 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
     expect(html).toContain('data-hz-topology-legend-title-owner="hertzbeat-ui-legend-title"');
     expect(html).toContain('data-hz-topology-legend-summary-owner="hertzbeat-ui-legend-summary"');
-	    expect(html).toContain('data-hz-topology-legend-section="status"');
-	    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
-	    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
-	    expect(html).toContain('data-hz-topology-legend-section="node-type"');
-	    expect(html).toContain('data-hz-topology-legend-section="interaction"');
-	    expect(html).not.toContain('data-hz-topology-legend-section="source-kind"');
-	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
-	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
-	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
-	    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
-	    expect(html).toContain('data-hz-topology-legend-item="warning-node"');
-	    expect(html).toContain('data-hz-topology-legend-item="critical-node"');
-    expect(html).toContain('data-hz-topology-legend-item="selected-node"');
-    expect(html).toContain('data-hz-topology-legend-item="directional-edge"');
-    expect(html).toContain('data-hz-topology-legend-item="dimmed-edge"');
-	    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
-	    expect(html).toContain('data-hz-topology-legend-color="#f59e0b"');
-	    expect(html).toContain('data-hz-topology-legend-color="#ef4444"');
-	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
-	    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
-	    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
-	    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
-    expect(html).toContain('data-hz-topology-legend-item-value-owner="hertzbeat-ui-legend-item-value"');
+    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).not.toContain('data-hz-topology-legend-section="status"');
+    expect(html).not.toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-section="source-kind"');
+    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
+    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
+    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+    expect(html).not.toContain('data-hz-topology-legend-item="healthy-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="warning-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="critical-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="selected-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="directional-edge"');
+    expect(html).not.toContain('data-hz-topology-legend-item="dimmed-edge"');
+    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+    expect(html).not.toContain('data-hz-topology-legend-item-value-owner="hertzbeat-ui-legend-item-value"');
     expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"');
     expect(html).toContain('data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"');
@@ -1151,7 +1271,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-action-link-copy-owner="hertzbeat-ui-action-link-copy"');
     expect(html).toContain('查看实体详情');
     expect(html).toContain('打开三信号');
-    expect(html).toContain('data-topology-node-select-href="/topology?');
+    expect(html).toContain('data-topology-node-select-mode="in-page-drawer"');
+    expect(html).toContain('data-topology-node-select-url-policy="preserve-current-url"');
+    expect(html).not.toContain('data-topology-node-select-href=');
     expect(html).toContain('data-topology-node-entity-href="/entities/service%3Acommerce%2Fcheckout?');
     expect(html).toContain('data-topology-context-link="entity"');
     expect(html).toContain('data-topology-context-link="metrics"');
@@ -1260,7 +1382,7 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-edge-id="relation-700"');
     expect(html).not.toContain('data-topology-node-id="svc-checkout"');
     expect(html).not.toContain('checkout-api');
-  }, 30000);
+  }, 60000);
 
   it('maps focused 1-hop routes into shared FocusTrail handoff metadata and scoped exit hrefs', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1287,14 +1409,22 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-focus-trail-depth="1"');
     expect(html).toContain('data-hz-topology-focus-trail-entity="svc-checkout"');
     expect(html).toContain('data-hz-topology-focus-crumb-active="true"');
+    expect(html).toContain('data-topology-depth-state="1"');
+    expect(html).toMatch(
+      /<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">1 跳<\/span>/
+    );
+    expect(html).not.toMatch(
+      /<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">2 跳<\/span>/
+    );
     expect(html).toContain('data-hz-topology-focus-filter="source"');
     expect(html).toContain('data-topology-focus-filter-source="cmdb-manual-label"');
     expect(html).toMatch(
-      /<a(?=[^>]*data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit")(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*environment=prod)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*timeRange=last-30m)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*sourceKind=cmdb-manual-label)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*groupBy=source-kind)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*depth=2)(?=[^>]*data-hz-topol [...]
+      /<a(?=[^>]*data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit")(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*environment=prod)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*timeRange=last-30m)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*sourceKind=cmdb-manual-label)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*groupBy=source-kind)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*depth=1)(?=[^>]*data-hz-topol [...]
     );
+    expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*depth=2/);
     expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*entityId=/);
     expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*serviceName=/);
-  }, 30000);
+  }, 60000);
 
   it('forwards groupBy route scope into the shared G6 grouping metadata and controls', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1320,6 +1450,10 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-group-surface-owner="hertzbeat-ui-g6-group-surface"');
     expect(html).toContain('data-hz-topology-g6-group-surface-visibility="assistive"');
     expect(html).not.toContain('absolute left-3 top-12 flex');
+    expect(html).toContain('data-hz-topology-g6-filter-visible-node-count="2"');
+    expect(html).toContain('data-hz-topology-g6-filter-visible-edge-count="1"');
+    expect(html).toContain('2 个可见节点 · 1 条可见边');
+    expect(html).not.toContain('7 个可见节点 · 7 条可见边');
     expect(html).toContain('data-hz-topology-g6-group-item="otlp-trace-call"');
     expect(html).toContain('data-hz-topology-g6-group-item-action-owner="hertzbeat-ui-g6-group-item-action"');
     expect(html).toMatch(
@@ -1343,17 +1477,17 @@ describe('topology page', () => {
       /<a(?=[^>]*data-topology-view-mode="resource-dependency")(?=[^>]*href="\/topology\?[^"]*viewMode=resource-dependency)(?=[^>]*href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
     );
     expect(html).toMatch(
-      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-hz-topology-g6-metadata-role="selection-button")(?=[^>]*data-topology-node-select-href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
+      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-hz-topology-g6-metadata-role="selection-button")(?=[^>]*data-topology-node-select-mode="in-page-drawer")(?=[^>]*data-topology-node-select-url-policy="preserve-current-url")[^>]*>/
     );
     expect(html).toContain('data-hz-topology-g6-focus-entry="double-click-node"');
     expect(html).toContain('data-hz-topology-g6-focus-depth-target="1-hop"');
     expect(html).toMatch(
-      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-topology-node-select-href="\/topology\?[^"]*depth=2)(?=[^>]*data-topology-node-focus-href="\/topology\?[^"]*depth=1)[^>]*>/
+      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-topology-node-select-mode="in-page-drawer")(?=[^>]*data-topology-node-focus-href="\/topology\?[^"]*depth=1)[^>]*>/
     );
     expect(html).toMatch(
       /data-topology-current-entity-panel="svc-checkout"[^>]*data-topology-current-entity-group-by="source-kind"[^>]*data-topology-current-entity-source-kind="otlp-trace-call"[^>]*data-topology-current-entity-view-mode="application"/
     );
-  }, 30000);
+  }, 60000);
 
   it('exposes focused API graph depth, relationship type, health, and evidence badges for browser verification', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1435,7 +1569,7 @@ describe('topology page', () => {
     expect(html).toMatch(/data-topology-node-id="entity-503"[^>]+data-topology-node-health="critical"[^>]+data-topology-node-evidence-badges="entity-relation database-middleware-connection two-hop"/);
     expect(html).toMatch(/data-topology-edge-id="relation-701"[^>]+data-topology-edge-relationship-type="trace-call"[^>]+data-topology-edge-evidence-badges="entity-relation otlp-trace-call one-hop"/);
     expect(html).toMatch(/data-topology-edge-id="relation-702"[^>]+data-topology-edge-relationship-type="database-connection"[^>]+data-topology-edge-evidence-badges="entity-relation database-middleware-connection two-hop"/);
-  }, 30000);
+  }, 60000);
 
   it('exposes trace-derived service graph RED metrics on nodes, edges, and selected evidence', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1549,6 +1683,21 @@ describe('topology page', () => {
     expect(html).toContain('7.25/s');
     expect(html).toContain('2.1%');
     expect(html).toContain('123 ms');
+    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-database"');
+    expect(html).not.toContain('data-hz-topology-legend-section="status"');
+    expect(html).not.toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-item="healthy-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="warning-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="critical-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="selected-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="directional-edge"');
+    expect(html).not.toContain('data-hz-topology-legend-item="dimmed-edge"');
+    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
+    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
     expect(html).toContain('data-topology-metric-table-owner="hertzbeat-ui"');
     expect(html).toContain('data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"');
     expect(html).toContain('data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"');
@@ -1562,6 +1711,10 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-metric-table-header-owner="hertzbeat-ui-metric-table-header"');
     expect(html).toContain('data-hz-topology-metric-rows="1"');
     expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"');
+    expect(html).toContain(
+      'data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"'
+    );
     expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="direct"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="inactive"');
@@ -1573,8 +1726,12 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-metric-table-filter-behavior="in-page-no-route-change"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
@@ -1585,6 +1742,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-edge-row-target-visible="true"');
     expect(html).toContain('data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"');
     expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-mode="table-row-click-drawer"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-edge-row="relation-701"');
     expect(html).toContain('data-hz-topology-edge-selected="true"');
     expect(html).toContain('data-hz-topology-metric-table-route-owner="hertzbeat-ui-metric-table-route"');
@@ -1614,26 +1774,262 @@ describe('topology page', () => {
     const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildLargeApiTopologyFixture()} />);
 
     expect(source).toContain('const topologyShouldShowRenderWindowMetricTable =');
+    expect(source).toContain('const topologyShouldRenderCompanionMetricTable = !topologyShouldShowRenderWindowMetricTable;');
     expect(source).toContain("topologyRenderWindowCompanion.mode === 'windowed'");
     expect(source).toContain("topologyRenderWindowCompanion.tableCompanion === 'required'");
+    expect(source).toContain('renderedEdgeCount: topologyG6RenderedEdgeCount');
+    expect(source).toContain('totalEdgeCount: topologyG6Graph.edges.length');
+    expect(source).toContain('const topologyG6HiddenEdgeCount = Math.max(0, topologyG6Graph.edges.length - topologyG6RenderedEdgeCount);');
     expect(html).toContain('data-hz-topology-g6-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-g6-render-window-total-node-count="230"');
     expect(html).toContain('data-hz-topology-g6-render-window-hidden-node-count="30"');
+    expect(html).toContain('data-hz-topology-g6-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-render-window-candidate-edge-count=');
+    expect(html).toContain(
+      'data-hz-topology-g6-render-window-rendered-edge-count-owner="hertzbeat-ui-g6-edge-density"'
+    );
+    expect(html).toContain('data-topology-g6-render-proof-owner="hertzbeat-topology-page-g6-proof"');
+    expect(html).toContain('data-topology-g6-render-window-mode="windowed"');
+    expect(html).toContain('data-topology-g6-render-window-total-node-count="230"');
+    expect(html).toContain('data-topology-g6-render-window-rendered-node-count="200"');
+    expect(html).toContain('data-topology-g6-render-window-total-edge-count="229"');
+    expect(html).toContain('data-topology-g6-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-topology-g6-render-window-hidden-edge-count="49"');
+    expect(html).toContain('data-topology-g6-summary-count-policy="windowed-rendered-vs-total"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-owner="hertzbeat-ui-g6-scale-performance"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-policy="windowed-interactive-budget"');
+    expect(html).toContain(
+      'data-hz-topology-g6-scale-performance-invariants="windowed-render pan-zoom-no-url-change no-remount no-refit viewport-preserved render-key-stable table-companion"'
+    );
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-node-budget="200"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-edge-budget="180"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-owner="hertzbeat-ui-g6-structure-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-length=');
+    expect(html).not.toContain('scale-svc-000:service|');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-owner="hertzbeat-ui-g6-render-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-length=');
+    expect(html).not.toContain('scale-svc-000::#');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state="pending"');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state-owner="hertzbeat-ui-g6-fit-settle-state"');
+    expect(html).toContain('data-hz-topology-g6-summary-count-policy="windowed-rendered-vs-total"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-g6-summary-total-node-count="230"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-summary-total-edge-count="229"');
+    expect(html).toContain('200/230 个节点已渲染 · 180/229 条边已渲染');
+    expect(html).not.toContain('230 个可见节点 · 229 条可见边');
     expect(html).toContain('data-topology-metric-table-placement="graph-bottom"');
     expect(html).toContain('data-topology-metric-table-visibility="render-window-companion"');
     expect(html).toContain('data-topology-metric-table-scope="edge-red-render-window"');
+    expect(html).toContain('data-topology-metric-table-dom-policy="single-interactive-table"');
+    expect(html).toContain('data-hz-topology-frame="workbench"');
+    expect(html).toContain('data-hz-topology-api-state="ready"');
+    expect(html).toContain('data-hz-topology-metric-table-root="true"');
+    expect(html).toContain('data-hz-topology-metric-table-total-rows="229"');
+    expect(html).not.toContain('data-topology-metric-table-scope="edge-red-companion"');
+    expect(html.match(/data-hz-ui="topology-metric-table"/g) ?? []).toHaveLength(1);
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-live-owner="hertzbeat-ui-g6-edge-density-affordance"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-invariants="table-scroll no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(source).toContain("edgeDensityDrilldownDetailLabel={t('topology.edge-density.hidden-in-table'");
+    expect(readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8')).toContain("'topology.edge-density.hidden-in-table'");
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-explanation-policy="render-window-not-missing-edges"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-detail-label=');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="230"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-hidden-node-count="30"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-total-edge-count="229"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-count-policy="canvas-rendered-vs-table-total"');
+    expect(html).toContain('data-hz-topology-metric-table-canvas-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-metric-table-table-edge-count="229"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-summary-owner="hertzbeat-ui-metric-table-edge-summary"');
+    expect(html).toContain('画布 180 / 表格 229');
+    expect(readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8')).toContain("'topology.metric-table.edge-summary'");
     expect(html).toContain('data-hz-topology-metric-table-render-window-table-companion="required"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-policy="windowed-dom-budget"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-budget="120"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-row-count="120"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-hidden-row-count="109"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-summary-owner="hertzbeat-ui-metric-table-row-render-summary"');
+    expect(html).toContain('显示 120 / 229 条关系');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
-    expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="hidden"');
+    expect(html).toContain('两端在画布');
+    expect(html).toContain('一端在画布');
+    expect(html).toContain('画布外');
+    expect(html).toContain('端点未知');
+    expect(html).not.toContain('半可见');
+    const renderedMetricRows = html.match(/data-hz-topology-edge-row="/g) ?? [];
+    const metricRowCountMatch = html.match(/data-hz-topology-metric-rows="(\d+)"/);
+    const metricRenderedRowCountMatch = html.match(/data-hz-topology-metric-table-rendered-row-count="(\d+)"/);
+    expect(metricRowCountMatch?.[1]).toBe('229');
+    expect(metricRenderedRowCountMatch?.[1]).toBe(String(renderedMetricRows.length));
     expect(html.indexOf('data-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"')).toBeLessThan(
       html.indexOf('data-topology-metric-table-placement="graph-bottom"')
     );
   }, 60000);
 
+  it('explains selected table edges that are only partly present in the current G6 render window', async () => {
+    const routeContext = {
+      environment: 'prod',
+      timeRange: 'last-1h',
+      sourceKind: 'otlp-trace-call',
+      viewMode: 'service-call',
+      groupBy: 'source-kind',
+      depth: '2',
+      edgeId: 'scale-svc-199--scale-svc-200'
+    };
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildLargeApiTopologyFixture()} />);
+
+    expect(html).toContain('data-topology-active-edge-id="scale-svc-199--scale-svc-200"');
+    expect(html).toContain('data-topology-edge-evidence-panel="scale-svc-199--scale-svc-200"');
+    expect(html).toContain('data-topology-edge-render-window-visibility="partial"');
+    expect(html).toContain('data-topology-edge-render-window-source-visible="true"');
+    expect(html).toContain('data-topology-edge-render-window-target-visible="false"');
+    expect(html).toContain('data-topology-edge-render-window-fact="partial"');
+    expect(html).toContain('data-topology-edge-render-window-fact-owner="hertzbeat-ui-detail-render-window-context"');
+    expect(html).toContain('画布窗口');
+    expect(html).toContain('一端在画布');
+  }, 60000);
+
+  it('does not turn an overview route into a focused graph when the API returns a default focus entity', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'cmdb-manual-label',
+          viewMode: 'application',
+          groupBy: 'source-kind',
+          depth: '2'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 501,
+          depth: 2,
+          sourceKinds: ['cmdb-manual-label'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'Payment API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['entity-relation', 'manual']
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'Orders DB',
+              entityType: 'database',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation', 'manual']
+            }
+          ],
+          edges: []
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-active-node-id="none"');
+    expect(html).toContain('data-hz-topology-focus-trail-mode="overview"');
+    expect(html).toContain('data-hz-topology-focus-trail-entity="all"');
+    expect(html).toContain('data-topology-focus-state="all"');
+    expect(html).not.toContain('data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit"');
+    expect(html).not.toContain('data-topology-focus-trail-exit-owner="hertzbeat-ui-focus-trail-exit"');
+    expect(html).not.toContain('data-hz-topology-g6-selected-node-id="entity-501"');
+    expect(html).not.toMatch(
+      /data-hz-topology-focus-crumb-active="true"[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-label-owner="hertzbeat-ui-focus-trail-crumb-label">Payment API<\/span>/
+    );
+  }, 30000);
+
+  it('marks API-backed isolated nodes as a relationship evidence gap instead of implying G6 dropped edges', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          viewMode: 'application',
+          groupBy: 'source-kind',
+          depth: '2'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: null,
+          depth: 2,
+          sourceKinds: ['entity-relation'],
+          nodes: [
+            {
+              id: '642126742338816',
+              entityId: 642126742338816,
+              entityName: 'Payment API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['entity-relation']
+            },
+            {
+              id: '642126752570624',
+              entityId: 642126752570624,
+              entityName: 'Checkout API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation']
+            },
+            {
+              id: '642126762803200',
+              entityId: 642126762803200,
+              entityName: 'Orders DB',
+              entityType: 'database',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation']
+            }
+          ],
+          edges: []
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-relation-gap-state="nodes-without-edges"');
+    expect(html).toContain('data-topology-relation-gap-owner="hertzbeat-ui-relation-gap"');
+    expect(html).toContain('data-topology-relation-gap-node-count="3"');
+    expect(html).toContain('data-topology-relation-gap-edge-count="0"');
+    expect(html).toContain('data-topology-relation-gap-source-kind="all"');
+    expect(html).toContain('data-topology-relation-gap-api-source-kinds="entity-relation"');
+    expect(html).toContain('data-topology-relation-gap-view-mode="application"');
+    expect(html).toContain('data-topology-relation-gap-visual="canvas-annotation"');
+    expect(html).toContain('data-topology-relation-gap-canvas-policy="hide-isolated-node-only-graph"');
+    expect(html).toContain('data-hz-topology-g6-node-count="0"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="0"');
+    expect(html).toContain('data-topology-relation-gap-action="otlp-trace-call"');
+    expect(html).toContain('data-topology-relation-gap-action-owner="hertzbeat-ui-action-link"');
+    expect(html).toContain('data-topology-relation-gap-action-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-topology-relation-gap-action-view-mode="service-call"');
+    expect(html).toContain('sourceKind=otlp-trace-call');
+    expect(html).toContain('viewMode=service-call');
+    expect(html).toContain('relationType=trace-call');
+    expect(html).toContain('data-topology-empty-state="none"');
+    expect(html).toContain('未返回关系边');
+    expect(html).toContain('查看调用关系');
+  }, 60000);
+
   it('renders API-backed impact timeline evidence when topology returns change events', async () => {
     i18nState.locale = 'en-US';
     const { default: TopologyPage } = await import('./topology-page');
@@ -1740,7 +2136,8 @@ describe('topology page', () => {
           environment: 'prod',
           timeRange: 'last-1h',
           sourceKind: 'otlp-trace-call',
-          relationType: 'trace-call'
+          relationType: 'trace-call',
+          groupBy: 'source-kind'
         }}
         apiGraph={{
           apiBacked: true,
@@ -1761,6 +2158,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-primitive="empty-state"');
     expect(html).toContain('data-hz-topology-empty-boundary="canvas"');
     expect(html).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
+    expect(html).toContain('data-hz-topology-empty-boundary-visual="frameless-canvas"');
     expect(html).toContain('data-hz-topology-empty-kind="filtered-empty"');
     expect(html).toContain('data-hz-topology-empty-source="Greptime trace graph"');
     expect(html).toContain('data-hz-topology-empty-time-scope="last-1h"');
@@ -1772,7 +2170,36 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-depth="2"');
     expect(html).toContain('data-hz-topology-empty-result-count="0"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace"');
+    expect(html).toContain('data-hz-topology-empty-copy-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-minimap-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-summary-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-group-surface-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-surface-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-topology-toolbar-group-control-visibility="hidden-empty-graph"');
+    expect(html).not.toContain('data-hz-topology-control-group-owner="hertzbeat-ui-toolbar-group-control"');
+    expect(html).not.toContain('data-hz-topology-control="group-by"');
+    expect(html).not.toContain('data-hz-topology-state-item="group"');
+    expect(html).not.toContain('data-topology-group-state=');
+    expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*groupBy=source-kind/);
+    expect(html).not.toMatch(/<a(?=[^>]*data-hz-topology-control="reset-scope")(?=[^>]*href="[^"]*groupBy=source-kind)[^>]*>/);
+    expect(html).not.toContain('data-hz-topology-g6-toolbar-visible-actions="zoom-out zoom-in fit-view reset-view"');
+    expect(html).not.toContain('data-hz-topology-g6-action="zoom-out"');
+    expect(html).not.toContain('data-hz-topology-g6-action="fit-view"');
+    expect(html).not.toContain('data-hz-topology-g6-minimap="viewport-overview"');
+    expect(html).not.toContain('data-hz-topology-g6-summary-owner="hertzbeat-ui-g6-summary"');
+    expect(html).not.toContain('data-hz-topology-g6-group-surface-owner="hertzbeat-ui-g6-group-surface"');
+    expect(html).not.toContain('data-hz-topology-g6-filter-control-surface-owner="hertzbeat-ui-g6-filter-control-surface"');
+    expect(html).not.toContain('data-hz-topology-g6-filter-control="source-kind"');
+    expect(html).not.toContain('data-hz-ui="topology-companion-rail"');
+    expect(html).not.toContain('data-topology-companion-section=');
+    expect(html).not.toContain('data-topology-metric-table-owner="hertzbeat-ui"');
+    expect(html).not.toContain('data-hz-ui="topology-metric-table"');
+    expect(html).not.toContain('data-hz-ui="topology-detail-drawer"');
+    expect(html).not.toContain('data-topology-current-entity-panel=');
     expect(html).toContain('暂无 trace 调用拓扑证据');
+    expect(html).toContain('当前窗口无 trace 调用边。');
+    expect(html).not.toContain('Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。');
     expect(html).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
     expect(html).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
     expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
@@ -1781,11 +2208,44 @@ describe('topology page', () => {
     expect(html).not.toContain('data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).not.toContain('data-hz-ui="topology-legend"');
     expect(html).not.toContain('data-hz-topology-legend-section=');
+    expect(html).not.toContain('data-topology-fault-context="incoming-evidence"');
+    expect(html).not.toContain('data-hz-topology-evidence-list-kind="fault-context"');
+    expect(html).not.toContain('data-topology-group-panel="large-graph-grouping"');
+    expect(html).not.toContain('data-hz-ui="topology-group-panel"');
+    expect(html).not.toContain('data-hz-topology-group-panel-action="open-table"');
+    expect(html).not.toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('0 hidden by scope');
     expect(html).not.toContain('data-topology-node-id="svc-checkout"');
     expect(html).not.toContain('checkout-api');
     expect(html).not.toContain('redis');
   }, 30000);
 
+  it('does not fall back to the default API graph for non-numeric focused entity ids', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: 'missing-topology-entity',
+          entityName: 'Missing',
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'cmdb-manual-label',
+          viewMode: 'application',
+          depth: '1'
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-api-focus-resolution="unresolvable-client-focus"');
+    expect(html).toContain('data-topology-api-focus-resolution-owner="topology-route-api-scope"');
+    expect(html).toContain('data-hz-topology-g6-node-count="0"');
+    expect(html).not.toContain('data-topology-node-id=');
+    expect(html).not.toContain('Payment API');
+    expect(html).not.toContain('Orders DB');
+  }, 30000);
+
   it('shows explicit missing trace-call evidence instead of a node-only trace graph', async () => {
     const { default: TopologyPage } = await import('./topology-page');
     const html = renderToStaticMarkup(
@@ -1838,35 +2298,52 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-result-count="0"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace"');
     expect(html).toContain('暂无 trace 调用拓扑证据');
+    expect(html).toContain('当前窗口无 trace 调用边。');
+    expect(html).not.toContain('Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。');
     expect(html).toContain('data-hz-topology-g6-node-count="0"');
     expect(html).toContain('data-hz-topology-g6-edge-count="0"');
+    expect(html).not.toContain('data-hz-ui="topology-companion-rail"');
+    expect(html).not.toContain('data-topology-companion-section=');
+    expect(html).not.toContain('data-topology-metric-table-owner="hertzbeat-ui"');
+    expect(html).not.toContain('data-hz-ui="topology-metric-table"');
+    expect(html).not.toContain('data-hz-ui="topology-detail-drawer"');
+    expect(html).not.toContain('data-topology-current-entity-panel=');
   }, 15000);
 
   it('exposes the resolved topology API scope for Browser release-gate checks', async () => {
     const { default: TopologyPage } = await import('./topology-page');
-    const html = renderToStaticMarkup(
-      <TopologyPage
-        routeContext={{
-          environment: 'prod',
-          timeRange: 'last-1h',
-          sourceKind: 'otlp-trace-call',
-          viewMode: 'service-call',
-          depth: '2',
-          groupBy: 'source-kind'
-        }}
-        apiGraph={{
-          apiBacked: true,
-          focusEntityId: null,
-          depth: 2,
-          sourceKinds: ['otlp-trace-call'],
-          nodes: [],
-          edges: []
-        }}
-      />
-    );
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-1h',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: null,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
 
     expect(html).toContain('data-topology-api-scope-owner="hertzbeat-ui-workbench-frame"');
-    expect(html).toContain('data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;sourceKind=otlp-trace-call&amp;relationType=trace-call"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780218000000&amp;end=1780221600000"'
+    );
     expect(html).toContain('data-topology-api-scope-source-kind="otlp-trace-call"');
     expect(html).toContain('data-topology-api-scope-relation-type="trace-call"');
     expect(html).toContain('data-topology-api-scope-view-mode="service-call"');
@@ -1874,6 +2351,130 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-relation-type="trace-call"');
   }, 15000);
 
+  it('exposes a first-class local Greptime scale proof route without remembering a focused URL', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-7d',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind',
+            scaleProof: 'greptime-real'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: 646562420231424,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+
+    expect(html).toContain('data-topology-scale-proof="greptime-real"');
+    expect(html).toContain('data-topology-scale-proof-owner="topology-route-api-scope"');
+    expect(html).toContain('data-topology-scale-proof-focus-entity-id="646562420231424"');
+    expect(html).toContain('data-topology-scale-proof-api-policy="focused-real-greptime-scale-fixture"');
+    expect(html).toContain('data-topology-scale-proof-time-policy="fixed-seeded-greptime-window"');
+    expect(html).toContain('data-topology-scale-proof-window-start="1780344000000"');
+    expect(html).toContain('data-topology-scale-proof-window-end="1780352700000"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?focusEntityId=646562420231424&amp;depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780344000000&amp;end=1780352700000"'
+    );
+  }, 15000);
+
+  it('exposes the mixed star/mesh Greptime scale proof as a global short route', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-7d',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind',
+            scaleProof: 'mixed-star-mesh'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: null,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+
+    expect(html).toContain('data-topology-scale-proof="mixed-star-mesh"');
+    expect(html).toContain('data-topology-scale-proof-owner="topology-route-api-scope"');
+    expect(html).not.toContain('data-topology-scale-proof-focus-entity-id=');
+    expect(html).toContain('data-topology-scale-proof-api-policy="global-real-greptime-mixed-star-mesh-fixture"');
+    expect(html).toContain('data-topology-scale-proof-time-policy="fixed-seeded-greptime-window"');
+    expect(html).toContain('data-topology-scale-proof-window-start="1780344000000"');
+    expect(html).toContain('data-topology-scale-proof-window-end="1780352700000"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780344000000&amp;end=1780352700000"'
+    );
+  }, 15000);
+
+  it('does not let the mixed star/mesh proof drift out of the seeded Greptime window as time moves forward', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-06-02T04:30:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-7d',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind',
+            scaleProof: 'mixed-star-mesh'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: null,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+
+    expect(html).toContain('data-topology-scale-proof-time-policy="fixed-seeded-greptime-window"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780344000000&amp;end=1780352700000"'
+    );
+    expect(html).not.toContain('start=1779737400000');
+  }, 15000);
+
   it('keeps the default topology route API-owned while the client API is pending or unavailable', async () => {
     const { default: TopologyPage } = await import('./topology-page');
     const pendingHtml = renderToStaticMarkup(<TopologyPage routeContext={{ environment: 'prod', timeRange: 'last-1h' }} />);
@@ -1901,8 +2502,28 @@ describe('topology page', () => {
         apiGraph={null}
       />
     );
+    const unauthorizedTraceCallHtml = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          relationType: 'trace-call'
+        }}
+        apiGraph={null}
+        apiGraphLoadError={{ status: 401, message: 'Topology API request failed: 401' }}
+      />
+    );
 
     expect(pendingHtml).toContain('data-topology-data-source="api"');
+    expect(pendingHtml).toContain('data-topology-api-settle-state="pending"');
+    expect(pendingHtml).toContain('data-topology-api-error-state="none"');
+    expect(pendingHtml).toContain('data-topology-api-error-status="none"');
+    expect(pendingHtml).toContain('data-topology-api-error-resolution="none"');
+    expect(pendingHtml).toContain('data-topology-api-settle-state-owner="hertzbeat-ui-workbench-frame"');
+    expect(pendingHtml).toContain('data-topology-relation-gap-state="api-pending"');
+    expect(pendingHtml).toContain('data-topology-relation-gap-visual="loading-state"');
+    expect(pendingHtml).toContain('data-topology-relation-gap-canvas-policy="loading-overlay"');
     expect(pendingHtml).toContain('data-topology-loading-state="api-pending"');
     expect(pendingHtml).toContain('data-topology-loading-state-owner="hertzbeat-ui-loading-state"');
     expect(pendingHtml).toContain('data-hz-ui="topology-loading-state"');
@@ -1915,6 +2536,9 @@ describe('topology page', () => {
     expect(pendingHtml).not.toContain('data-topology-empty-state="api-empty"');
     expect(pendingTraceCallHtml).toContain('data-topology-trace-call-state="pending"');
     expect(pendingTraceCallHtml).toContain('data-topology-trace-call-red-state="pending"');
+    expect(unavailableHtml).toContain('data-topology-api-settle-state="degraded"');
+    expect(unavailableHtml).toContain('data-topology-api-error-state="unknown"');
+    expect(unavailableHtml).toContain('data-topology-api-error-resolution="retry-or-check-api"');
 
     [pendingHtml, unavailableHtml].forEach(html => {
       expect(html).toContain('data-topology-data-source="api"');
@@ -1931,6 +2555,7 @@ describe('topology page', () => {
     expect(unavailableHtml).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-copy-visibility="assistive"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-kind="degraded"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-source="拓扑 API 不可用"');
@@ -1942,6 +2567,13 @@ describe('topology page', () => {
     expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-source-kind="otlp-trace-call"');
     expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-relation-type="trace-call"');
     expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace unavailable"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-settle-state="degraded"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-error-state="unauthorized"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-error-status="401"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-error-resolution="login-required"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-trace-call-state="degraded"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-empty-state="trace-call-degraded"');
+    expect(unauthorizedTraceCallHtml).not.toContain('data-topology-empty-state="trace-call-missing"');
   }, 30000);
 
   it('uses incoming entity context to seed filters, active topology node, and current signal links', async () => {
@@ -1966,6 +2598,14 @@ describe('topology page', () => {
     expect(html).toContain('last-30m');
     expect(html).toContain('timeRange=last-30m');
     expect(html).toContain('当前筛选');
+    expect(html).toContain('data-topology-view-scope="focused-adjacency"');
+    expect(html).toContain('data-topology-view-scope-owner="hertzbeat-ui-focus-scope-guide"');
+    expect(html).toContain('data-topology-view-scope-global-action="open-global-topology"');
+    expect(html).toContain('data-topology-view-scope-global-href="/topology?');
+    expect(html).toContain('当前为局部聚焦视角');
+    expect(html).toContain('查看全局拓扑');
+    expect(html).not.toMatch(/data-topology-view-scope-global-href="[^"]*entityId=/);
+    expect(html).not.toMatch(/data-topology-view-scope-global-href="[^"]*serviceName=/);
   }, 15000);
 
   it('uses view mode and source query params to narrow topology and expose alert-impact closure', async () => {
@@ -1975,9 +2615,21 @@ describe('topology page', () => {
       environment: 'prod',
       timeRange: 'last-1h'
     };
+    const alertImpactGraph = buildApiTopologyFixture();
+    alertImpactGraph.sourceKinds = [...alertImpactGraph.sourceKinds, 'alert-impact'];
+    alertImpactGraph.edges = alertImpactGraph.edges.map(edge =>
+      edge.sourceNodeId === 'svc-checkout' && edge.targetNodeId === 'res-orders-db'
+        ? {
+            ...edge,
+            relationType: 'alert impact',
+            relationSource: 'alert-impact',
+            evidenceBadges: ['alert-impact']
+          }
+        : edge
+    );
 
     const { default: TopologyPage } = await import('./topology-page');
-    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildApiTopologyFixture()} />);
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={alertImpactGraph} />);
 
     expect(html).toContain('data-topology-active-view-mode="alert-impact"');
     expect(html).toContain('data-topology-active-source-kind="alert-impact"');
@@ -1985,8 +2637,11 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-control-source-kind-value="alert-impact"');
     expect(html).not.toContain('data-topology-source-active="alert-impact"');
     expect(html).toContain('data-topology-edge-focus="active-path"');
-    expect(html).toContain('data-topology-edge-focus="context-muted"');
-    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-focus="active"/);
+    expect(html).toContain('data-hz-topology-g6-edge-count="1"');
+    expect(html).toContain('data-topology-edge-source="alert-impact"');
+    expect(html).not.toContain('data-topology-edge-source="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-path-source-kind="alert-impact"');
+    expect(html).not.toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-focus="active"/);
     expect(html).toContain('data-topology-alert-impact-link="alert-center"');
     expect(html).toContain('data-topology-alert-impact-link-owner="hertzbeat-ui-action-link"');
     expect(html).toContain('data-hz-topology-action-link-label-owner="hertzbeat-ui-action-link-label"');
@@ -1995,12 +2650,80 @@ describe('topology page', () => {
     expect(html).toContain('source=topology');
     expect(html).toContain('viewMode=alert-impact');
     expect(html).toContain('sourceKind=alert-impact');
-    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-select-href="\/topology\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
+    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-select-mode="in-page-drawer"/);
     expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-entity-href="\/entities\/service%3Acommerce%2Fcheckout\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="entity"[^>]+href="\/entities\/service%3Acommerce%2Fcheckout\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="metrics"[^>]+href="\/ingestion\/otlp\/metrics\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="logs"[^>]+href="\/log\/manage\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="traces"[^>]+href="\/trace\/manage\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
+    expect(html).not.toContain('data-topology-current-entity-panel="svc-checkout"');
+    expect(html).not.toContain('data-topology-context-link="entity"');
+    expect(html).not.toContain('data-topology-context-link="metrics"');
+    expect(html).not.toContain('data-topology-context-link="logs"');
+    expect(html).not.toContain('data-topology-context-link="traces"');
+  }, 15000);
+
+  it('does not render unrelated trace-call graph evidence for an explicit alert-impact source filter', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          viewMode: 'alert-impact',
+          sourceKind: 'alert-impact',
+          environment: 'prod',
+          timeRange: 'last-1h'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 'service:commerce/payment',
+          depth: 1,
+          sourceKinds: ['otlp-trace-call'],
+          nodes: [
+            {
+              id: 'svc-checkout',
+              entityId: 'service:commerce/checkout',
+              entityName: 'Checkout API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['otlp-trace-call']
+            },
+            {
+              id: 'svc-payment',
+              entityId: 'service:commerce/payment',
+              entityName: 'Payment API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['otlp-trace-call']
+            }
+          ],
+          edges: [
+            {
+              sourceNodeId: 'svc-checkout',
+              targetNodeId: 'svc-payment',
+              relationType: 'trace-call',
+              relationSource: 'otlp-trace-call',
+              status: 'warning',
+              score: 42,
+              evidenceBadges: ['otlp-trace-call'],
+              redMetrics: {
+                requestRatePerSecond: 5,
+                errorRate: 0.02,
+                latencyP95Ms: 180
+              }
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-active-source-kind="alert-impact"');
+    expect(html).toContain('data-topology-empty-state="filtered-empty"');
+    expect(html).toContain('data-hz-topology-empty-source-kind="alert-impact"');
+    expect(html).toContain('data-hz-topology-empty-result-count="0"');
+    expect(html).toContain('data-hz-topology-g6-node-count="0"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="0"');
+    expect(html).not.toContain('data-hz-topology-g6-edge-source="otlp-trace-call"');
   }, 15000);
 
   it('renders selected edge evidence and drilldown links with retained topology context', async () => {
@@ -2019,6 +2742,11 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-edge-selected="true"');
     expect(html).toMatch(/data-topology-edge-id="svc-checkout--res-orders-db"[^>]+data-topology-edge-focus="active-path"/);
     expect(html).toContain('data-topology-edge-evidence-panel="svc-checkout--res-orders-db"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
+    expect(html.indexOf('data-topology-edge-evidence-panel="svc-checkout--res-orders-db"')).toBeLessThan(
+      html.indexOf('data-topology-companion-section="view-mode"')
+    );
     expect(html).toContain('关系证据');
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
@@ -2056,6 +2784,29 @@ describe('topology page', () => {
 
     expect(html).toContain('data-topology-current-entity-panel="svc-checkout"');
     expect(html).toContain('data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"');
+    expect(html).toContain('data-topology-current-entity-panel-hover-continuity="selected-node-survives-edge-hover"');
+    expect(html).toContain('data-topology-current-entity-panel-hover-invariants="selected-node-detail edge-hover-path-summary-temporary pointer-leave-clears-hover no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-scroll-continuity="selected-node-retakes-companion-first-viewport"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-scroll-invariants="manual-active-resets-scroll-active active-section-scroll collapse-state-preserved no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-fit-invariants="bounded-block internal-scroll no-page-scroll no-rail-overflow scan-first-viewport"');
+    expect(html).toContain('data-topology-current-entity-panel-signal-entry="header-dock"');
+    expect(html).toContain('data-topology-current-entity-panel-signal-entry-invariants="metrics-logs-traces-before-long-evidence sticky-with-node-context no-extra-navigation"');
+    expect(html).toContain('data-topology-current-entity-panel-scroll-reset="identity-change"');
+    expect(html).toContain('data-topology-current-entity-panel-scroll-reset-invariants="identity-change scroll-top-zero no-page-scroll no-rail-scroll no-url-change"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement="header-dock"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-sticky="top-with-header-context"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
+    expect(html.indexOf('data-topology-current-entity-panel="svc-checkout"')).toBeLessThan(
+      html.indexOf('data-topology-companion-section="view-mode"')
+    );
     expect(html).toContain('data-topology-current-entity-upstream-count="4"');
     expect(html).toContain('data-topology-current-entity-downstream-count="2"');
     expect(html).toContain('data-topology-current-entity-upstream-node-ids="app-commerce svc-frontend monitor-checkout k8s-checkout-workload"');
diff --git a/web-next/app/topology/page.tsx b/web-next/app/topology/page.tsx
index c30aab4f96..98460926b4 100644
--- a/web-next/app/topology/page.tsx
+++ b/web-next/app/topology/page.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
-import TopologyPage from './topology-page';
+import TopologyRouteClient from './topology-route-client';
+import TopologyRouteShell from './topology-route-shell';
 import { readTopologyRouteContext, type TopologySearchParams } from '../../lib/topology-surface/query-state';
 
 export default async function TopologyRoutePage({
@@ -9,5 +10,12 @@ export default async function TopologyRoutePage({
 } = {}) {
   const resolvedSearchParams = await searchParams;
   const routeContext = readTopologyRouteContext(resolvedSearchParams);
-  return <TopologyPage routeContext={routeContext} />;
+  return (
+    <>
+      <div id="topology-route-deferred-shell">
+        <TopologyRouteShell />
+      </div>
+      <TopologyRouteClient routeContext={routeContext} shellElementId="topology-route-deferred-shell" />
+    </>
+  );
 }
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index 20ec9f0449..94544929c0 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -33,15 +33,19 @@ import {
   type HzTopologyDetailDrawerAction,
   type HzTopologyDetailDrawerFact,
   type HzTopologyGroupPanelItem,
+  type HzTopologyLegendItem,
+  type HzTopologyLegendSection,
   type HzTopologyPathSummaryMetric,
   type HzTopologyHoverTooltipFact,
   type HzTopologyHoverTooltipMetric,
   type HzTopologyMetricRow,
+  type HzTopologyMetricRowWindowVisibility,
   type HzTopologyMetricTableRenderWindowFilter,
   type HzTopologyToolbarStateItem
-} from '@hertzbeat/ui';
+} from '@hertzbeat/ui/topology';
 import {
   HzTopologyG6Canvas,
+  HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT,
   HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
@@ -51,7 +55,17 @@ import {
 } from '@hertzbeat/ui/topology-g6';
 import { useI18n } from '../../components/providers/i18n-provider';
 import { api } from '../../lib/api-facade';
-import { buildTopologyApiUrl, loadTopologyGraph, resolveTopologyRelationType } from '../../lib/topology-surface/controller';
+import {
+  buildTopologyApiUrl,
+  hasUnresolvableTopologyFocusEntity,
+  loadTopologyGraph,
+  resolveTopologyApiTimeoutMs,
+  resolveTopologyRelationType,
+  resolveTopologyScaleProofApiPolicy,
+  resolveTopologyScaleProofFocusEntityId,
+  resolveTopologyScaleProofTimeWindow,
+  shouldPreservePreviousTopologyGraphDuringLoad
+} from '../../lib/topology-surface/controller';
 import {
   buildTopologyServiceMapFromApiGraph,
   type EntityTopologyApiGraph,
@@ -64,6 +78,12 @@ import {
 
 type TopologyLocalSelectionSource = 'none' | 'node-click' | 'edge-click' | 'table-row-click';
 type TopologyLayoutMode = 'layered-service' | 'force' | 'grid-table';
+type TopologyApiLoadError = {
+  status?: number;
+  code?: number;
+  message?: string;
+};
+type TopologyApiErrorState = 'none' | 'unauthorized' | 'api-error' | 'unknown';
 
 function findNode(nodes: TopologyServiceNode[], id: string) {
   return nodes.find(node => node.id === id);
@@ -73,6 +93,27 @@ function findEdge(edges: TopologyServiceEdge[], id: string) {
   return edges.find(edge => edge.id === id);
 }
 
+function topologyEdgeMatchesCanvasView(edge: TopologyServiceEdge, viewMode: TopologyFilterContext['viewMode']) {
+  if (viewMode === 'application') return true;
+  if (viewMode === 'service-call') return edge.relationshipType === 'trace-call';
+  if (viewMode === 'alert-impact') return edge.alertImpact !== 'none' || edge.source === 'alert-impact';
+  return (
+    edge.relationshipType === 'template-dependency' ||
+    edge.relationshipType === 'k8s-ownership' ||
+    edge.relationshipType === 'database-connection' ||
+    edge.relationshipType === 'middleware-connection'
+  );
+}
+
+function topologyEdgeMatchesCanvasSource(edge: TopologyServiceEdge, sourceKind: TopologyFilterContext['sourceKind']) {
+  if (!sourceKind) return true;
+  return edge.source === sourceKind;
+}
+
+function topologyEdgeMatchesCanvasScope(edge: TopologyServiceEdge, filterContext: TopologyFilterContext) {
+  return topologyEdgeMatchesCanvasView(edge, filterContext.viewMode) && topologyEdgeMatchesCanvasSource(edge, filterContext.sourceKind);
+}
+
 function resolveTopologyG6EntityType(node: Pick<TopologyServiceNode, 'entityType' | 'id' | 'label' | 'source'>) {
   const inferredIcon = getHzTopologyG6NodeIcon(`${node.entityType} ${node.label} ${node.source} ${node.id}`);
   if (inferredIcon.kind === 'cache' || inferredIcon.kind === 'queue') return inferredIcon.kind;
@@ -345,8 +386,9 @@ function hasTraceRedMetrics(edge: TopologyServiceEdge) {
   ].some(value => typeof value === 'number' && Number.isFinite(value));
 }
 
-function pickTopologyDetailEdge(map: ReturnType<typeof buildTopologyServiceMapFromApiGraph>) {
-  return map.selectedEdge ?? map.edges.find(hasTraceRedMetrics) ?? map.edges[0];
+function pickTopologyDetailEdge(map: ReturnType<typeof buildTopologyServiceMapFromApiGraph>, edges: TopologyServiceEdge[] = map.edges) {
+  const selectedEdge = map.selectedEdge ? edges.find(edge => edge.id === map.selectedEdge?.id) : undefined;
+  return selectedEdge ?? edges.find(hasTraceRedMetrics) ?? edges[0];
 }
 
 function appendTopologySelectionParam(params: URLSearchParams, key: string, value: string | undefined) {
@@ -417,11 +459,76 @@ const TOPOLOGY_EDGE_FACT_IDS = [
   'last-seen'
 ];
 
+type TopologyEdgeRenderWindowContext = {
+  visibility: HzTopologyMetricRowWindowVisibility;
+  sourceVisible: 'true' | 'false' | 'unknown';
+  targetVisible: 'true' | 'false' | 'unknown';
+};
+
+function resolveTopologyEdgeEndpointRenderWindowVisibility(
+  nodeId: string | undefined,
+  renderedNodeIds: Set<string> | undefined
+): 'true' | 'false' | 'unknown' {
+  if (!nodeId || !renderedNodeIds) return 'unknown';
+  return renderedNodeIds.has(nodeId) ? 'true' : 'false';
+}
+
+function resolveTopologyEdgeRenderWindowVisibility(
+  sourceVisible: TopologyEdgeRenderWindowContext['sourceVisible'],
+  targetVisible: TopologyEdgeRenderWindowContext['targetVisible']
+): HzTopologyMetricRowWindowVisibility {
+  if (sourceVisible === 'unknown' || targetVisible === 'unknown') return 'unknown';
+  if (sourceVisible === 'true' && targetVisible === 'true') return 'visible';
+  if (sourceVisible === 'false' && targetVisible === 'false') return 'hidden';
+  return 'partial';
+}
+
+function buildTopologyEdgeRenderWindowContext(
+  edge: TopologyServiceEdge | undefined,
+  renderWindowMode: 'direct' | 'windowed',
+  renderedNodeIds: string[] | undefined
+): TopologyEdgeRenderWindowContext | undefined {
+  if (!edge || renderWindowMode !== 'windowed') return undefined;
+  const renderedNodeIdSet = renderedNodeIds?.length ? new Set(renderedNodeIds) : undefined;
+  const sourceVisible = resolveTopologyEdgeEndpointRenderWindowVisibility(edge.from, renderedNodeIdSet);
+  const targetVisible = resolveTopologyEdgeEndpointRenderWindowVisibility(edge.to, renderedNodeIdSet);
+  return {
+    visibility: resolveTopologyEdgeRenderWindowVisibility(sourceVisible, targetVisible),
+    sourceVisible,
+    targetVisible
+  };
+}
+
+function resolveTopologyEdgeRenderWindowLabel(
+  visibility: HzTopologyMetricRowWindowVisibility,
+  t: (key: string) => string
+) {
+  if (visibility === 'visible') return t('topology.metric-table.filter.visible');
+  if (visibility === 'partial') return t('topology.metric-table.filter.partial');
+  if (visibility === 'hidden') return t('topology.metric-table.filter.hidden');
+  return t('topology.metric-table.filter.unknown');
+}
+
 function buildTopologyEdgeDetailFacts(
   edge: TopologyServiceEdge,
-  t: (key: string) => string
+  t: (key: string) => string,
+  renderWindowContext?: TopologyEdgeRenderWindowContext
 ): HzTopologyDetailDrawerFact[] {
   return [
+    ...(renderWindowContext
+      ? [{
+          id: 'render-window-context',
+          label: t('topology.edge.render-window.label'),
+          value: resolveTopologyEdgeRenderWindowLabel(renderWindowContext.visibility, t),
+          tone: renderWindowContext.visibility === 'visible' ? 'info' as const : 'warning' as const,
+          factProps: {
+            'data-topology-edge-render-window-fact': renderWindowContext.visibility,
+            'data-topology-edge-render-window-fact-owner': 'hertzbeat-ui-detail-render-window-context',
+            'data-topology-edge-render-window-source-visible': renderWindowContext.sourceVisible,
+            'data-topology-edge-render-window-target-visible': renderWindowContext.targetVisible
+          } as React.HTMLAttributes<HTMLDivElement>
+        }]
+      : []),
     ...edge.evidence.rows.map((row, index) => ({
       id: TOPOLOGY_EDGE_FACT_IDS[index] ?? `evidence-${index + 1}`,
       label: row.label,
@@ -636,48 +743,105 @@ function buildApiOwnedEmptyGraph(routeContext?: TopologyRouteContext): EntityTop
   };
 }
 
-const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;
-const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;
+function normalizeTopologyApiLoadError(error: unknown): TopologyApiLoadError | null {
+  if (!error || typeof error !== 'object') return null;
+  const candidate = error as { status?: unknown; code?: unknown; message?: unknown };
+  const status = typeof candidate.status === 'number' ? candidate.status : undefined;
+  const code = typeof candidate.code === 'number' ? candidate.code : undefined;
+  const message = typeof candidate.message === 'string' ? candidate.message : undefined;
+  return { status, code, message };
+}
+
+function resolveTopologyApiErrorState(error: TopologyApiLoadError | null | undefined): TopologyApiErrorState {
+  if (!error) return 'none';
+  if (error.status === 401 || error.code === 401) return 'unauthorized';
+  if (error.status || error.code) return 'api-error';
+  return 'unknown';
+}
 
-function resolveTopologyApiTimeoutMs(routeContext?: TopologyRouteContext) {
-  if (routeContext?.sourceKind === 'otlp-trace-call' || routeContext?.viewMode === 'service-call') {
-    return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;
-  }
-  return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;
+function resolveTopologyApiErrorResolution(state: TopologyApiErrorState) {
+  if (state === 'none') return 'none';
+  if (state === 'unauthorized') return 'login-required';
+  return 'retry-or-check-api';
 }
 
 export default function TopologyPage({
   routeContext,
-  apiGraph
+  apiGraph,
+  apiGraphLoadError
 }: {
   routeContext?: TopologyRouteContext;
   apiGraph?: EntityTopologyApiGraph | null;
+  apiGraphLoadError?: TopologyApiLoadError | null;
 } = {}) {
   const topologyRouter = useRouter();
   const { t } = useI18n();
   const routeContextKey = React.useMemo(() => JSON.stringify(routeContext ?? {}), [routeContext]);
-  const [loadedApiGraph, setLoadedApiGraph] = React.useState<EntityTopologyApiGraph | null | undefined>(apiGraph);
-  const [topologyLayout] = React.useState<TopologyLayoutMode>('layered-service');
-  const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(undefined);
   const apiOwnedEmptyGraph = React.useMemo(() => buildApiOwnedEmptyGraph(routeContext), [routeContext]);
+  const topologyHasUnresolvableFocusEntity = React.useMemo(
+    () => hasUnresolvableTopologyFocusEntity(routeContext),
+    [routeContext]
+  );
+  const [loadedApiGraph, setLoadedApiGraph] = React.useState<EntityTopologyApiGraph | null | undefined>(() =>
+    apiGraph !== undefined ? apiGraph : topologyHasUnresolvableFocusEntity ? apiOwnedEmptyGraph : undefined
+  );
+  const [loadedApiGraphError, setLoadedApiGraphError] = React.useState<TopologyApiLoadError | null>(() =>
+    apiGraph === null ? (apiGraphLoadError ?? {}) : null
+  );
+  const [topologyLayout] = React.useState<TopologyLayoutMode>('layered-service');
+  const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(routeContext?.search);
+  const [topologyManualRefreshSequence, setTopologyManualRefreshSequence] = React.useState(0);
+
+  const handleTopologyRefresh = React.useCallback(() => {
+    setTopologyManualRefreshSequence(sequence => sequence + 1);
+  }, []);
 
   React.useEffect(() => {
-    if (apiGraph !== undefined) {
+    if (apiGraph !== undefined && topologyManualRefreshSequence === 0) {
       setLoadedApiGraph(apiGraph);
+      setLoadedApiGraphError(apiGraph === null ? (apiGraphLoadError ?? {}) : null);
+      return undefined;
+    }
+    if (topologyHasUnresolvableFocusEntity) {
+      setLoadedApiGraph(apiOwnedEmptyGraph);
+      setLoadedApiGraphError(null);
       return undefined;
     }
     let mounted = true;
-    void loadTopologyGraph(api.topology.request, routeContext ?? {}, { timeoutMs: resolveTopologyApiTimeoutMs(routeContext) })
+    const topologyShouldPreservePreviousGraphDuringLoad =
+      topologyManualRefreshSequence === 0 && shouldPreservePreviousTopologyGraphDuringLoad(routeContext ?? {});
+    if (!topologyShouldPreservePreviousGraphDuringLoad) {
+      setLoadedApiGraph(undefined);
+      setLoadedApiGraphError(null);
+    }
+    void loadTopologyGraph(api.topology.request, routeContext ?? {}, {
+      timeoutMs: resolveTopologyApiTimeoutMs(routeContext),
+      ...(topologyManualRefreshSequence > 0 ? { cacheBust: () => `manual-${topologyManualRefreshSequence}` } : {})
+    })
       .then(graph => {
-        if (mounted) setLoadedApiGraph(graph);
+        if (mounted) {
+          setLoadedApiGraph(graph);
+          setLoadedApiGraphError(null);
+        }
       })
-      .catch(() => {
-        if (mounted) setLoadedApiGraph(null);
+      .catch(error => {
+        if (mounted) {
+          setLoadedApiGraph(null);
+          setLoadedApiGraphError(normalizeTopologyApiLoadError(error) ?? {});
+        }
       });
     return () => {
       mounted = false;
     };
-  }, [apiGraph, apiOwnedEmptyGraph, routeContext, routeContextKey]);
+  }, [
+    apiGraph,
+    apiGraphLoadError,
+    apiOwnedEmptyGraph,
+    routeContext,
+    routeContextKey,
+    topologyHasUnresolvableFocusEntity,
+    topologyManualRefreshSequence
+  ]);
 
   const map = React.useMemo(
     () => buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t),
@@ -694,13 +858,50 @@ export default function TopologyPage({
     loadedApiGraph !== null &&
     map.filterContext.sourceKind === 'otlp-trace-call' &&
     map.edges.length === 0;
+  const topologyHasExplicitCanvasScope = Boolean(routeContext?.sourceKind || routeContext?.viewMode || routeContext?.edgeId);
+  const topologyScopedCanvasEdges = React.useMemo(
+    () => (topologyHasExplicitCanvasScope && map.edges.length > 0 ? map.edges.filter(edge => topologyEdgeMatchesCanvasScope(edge, map.filterContext)) : map.edges),
+    [map.edges, map.filterContext, topologyHasExplicitCanvasScope]
+  );
+  const topologyFilteredEvidenceMissing =
+    map.dataSource === 'api' &&
+    !topologyIsPending &&
+    loadedApiGraph !== null &&
+    topologyHasExplicitCanvasScope &&
+    map.edges.length > 0 &&
+    topologyScopedCanvasEdges.length === 0;
+  const topologyRelationGap =
+    map.dataSource === 'api' &&
+    !topologyIsPending &&
+    loadedApiGraph !== null &&
+    map.nodes.length > 0 &&
+    map.edges.length === 0 &&
+    !topologyTraceCallMissingEdges &&
+    !topologyFilteredEvidenceMissing;
   const topologyCanvasNodes = React.useMemo(
-    () => (topologyTraceCallMissingEdges ? [] : map.nodes),
-    [map.nodes, topologyTraceCallMissingEdges]
+    () => {
+      if (topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing || topologyRelationGap) return [];
+      if (!topologyHasExplicitCanvasScope || map.edges.length === 0) return map.nodes;
+      const visibleNodeIds = new Set<string>();
+      topologyScopedCanvasEdges.forEach(edge => {
+        visibleNodeIds.add(edge.from);
+        visibleNodeIds.add(edge.to);
+      });
+      return map.nodes.filter(node => visibleNodeIds.has(node.id));
+    },
+    [
+      map.edges.length,
+      map.nodes,
+      topologyFilteredEvidenceMissing,
+      topologyHasExplicitCanvasScope,
+      topologyRelationGap,
+      topologyScopedCanvasEdges,
+      topologyTraceCallMissingEdges
+    ]
   );
   const topologyCanvasEdges = React.useMemo(
-    () => (topologyTraceCallMissingEdges ? [] : map.edges),
-    [map.edges, topologyTraceCallMissingEdges]
+    () => (topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing ? [] : topologyScopedCanvasEdges),
+    [topologyFilteredEvidenceMissing, topologyScopedCanvasEdges, topologyTraceCallMissingEdges]
   );
   const topologyTraceCallEdges = map.edges.filter(edge => edge.source === 'otlp-trace-call' || edge.relationshipType === 'trace-call');
   const topologyTraceCallRedEdgeCount = topologyTraceCallEdges.filter(edge =>
@@ -715,9 +916,11 @@ export default function TopologyPage({
     Boolean(edge.evidence.sampleTraceId || edge.evidence.sampleSpanId)
   ).length;
   const topologyTraceCallState =
-    topologyIsPending && topologyTraceCallScope
+    !topologyTraceCallScope
+      ? 'none'
+      : topologyIsPending
       ? 'pending'
-      : loadedApiGraph === null && topologyTraceCallScope
+      : loadedApiGraph === null
         ? 'degraded'
         : topologyTraceCallMissingEdges
           ? 'missing-edges'
@@ -734,9 +937,36 @@ export default function TopologyPage({
       : topologyTraceCallRedEdgeCount === topologyTraceCallEdges.length
         ? 'ready'
         : 'partial';
+  const topologyApiSettleState = topologyIsPending ? 'pending' : loadedApiGraph === null ? 'degraded' : 'ready';
+  const topologyApiErrorState = topologyIsPending || loadedApiGraph !== null
+    ? 'none'
+    : resolveTopologyApiErrorState(loadedApiGraphError);
+  const topologyApiErrorStatus = topologyApiErrorState === 'none'
+    ? 'none'
+    : loadedApiGraphError?.status ?? loadedApiGraphError?.code ?? 'unknown';
+  const topologyApiErrorResolution = resolveTopologyApiErrorResolution(topologyApiErrorState);
+  const topologyRelationGapState = topologyIsPending ? 'api-pending' : topologyRelationGap ? 'nodes-without-edges' : 'none';
+  const topologyRelationGapVisual = topologyIsPending ? 'loading-state' : topologyRelationGap ? 'canvas-annotation' : 'none';
+  const topologyRelationGapCanvasPolicy = topologyIsPending ? 'loading-overlay' : topologyRelationGap ? 'hide-isolated-node-only-graph' : 'render-graph';
+  const topologyHasRouteEntityFocus = Boolean(
+    routeContext?.entityId?.trim() ||
+    routeContext?.entityName?.trim() ||
+    routeContext?.serviceName?.trim() ||
+    routeContext?.monitorId?.trim() ||
+    routeContext?.monitorName?.trim() ||
+    routeContext?.traceId?.trim() ||
+    routeContext?.spanId?.trim()
+  );
+  const topologyCanShowApiFocus = routeContext === undefined || topologyHasRouteEntityFocus;
+  const topologyVisibleActiveNodeId = topologyCanShowApiFocus ? map.activeNodeId : undefined;
+  const topologyVisibleFocusQuery = topologyCanShowApiFocus ? topologyEffectiveSearchQuery : topologyToolbarSearchQuery;
   const topologyApiRequestPath = buildTopologyApiUrl(routeContext ?? {});
   const topologyApiRequestTimeoutMs = resolveTopologyApiTimeoutMs(routeContext);
   const topologyApiScopeRelationType = resolveTopologyRelationType(routeContext ?? {}) ?? 'all';
+  const topologyScaleProof = routeContext?.scaleProof?.trim();
+  const topologyScaleProofFocusEntityId = resolveTopologyScaleProofFocusEntityId(routeContext ?? {});
+  const topologyScaleProofApiPolicy = resolveTopologyScaleProofApiPolicy(routeContext ?? {});
+  const topologyScaleProofTimeWindow = resolveTopologyScaleProofTimeWindow(routeContext ?? {});
   const topologyG6Layout = topologyLayout === 'force' ? 'force' : 'layered-service';
   const topologyLayoutLabel =
     topologyLayout === 'force'
@@ -752,11 +982,10 @@ export default function TopologyPage({
       entityType: resolveTopologyG6EntityType(node),
       health: node.health,
       tone: node.tone,
-      focus: node.focus,
+      focus: topologyCanShowApiFocus ? node.focus : 'normal',
       source: node.source,
       evidenceBadges: node.evidenceBadges,
       redMetrics: node.redMetrics,
-      href: buildTopologyNodeSelectionHref(node, map, routeContext),
       focusHref: buildTopologyNodeSelectionHref(node, map, routeContext, { depth: 1 }),
       entityHref: node.links.entityHref
     })),
@@ -774,14 +1003,20 @@ export default function TopologyPage({
       redMetrics: edge.redMetrics,
       href: edge.drilldownHref
     }))
-  }), [map, routeContext, topologyCanvasEdges, topologyCanvasNodes]);
+  }), [map, routeContext, topologyCanShowApiFocus, topologyCanvasEdges, topologyCanvasNodes]);
+  const topologyHasRenderableGraph = topologyG6Graph.nodes.length > 0 || topologyG6Graph.edges.length > 0;
   React.useEffect(() => {
     setTopologyLocalSelection({ source: 'none' });
   }, [routeContextKey]);
   React.useEffect(() => {
-    setTopologySearchQuery(undefined);
-  }, [routeContextKey]);
-  const topologyRoutePrimaryNode = map.nodes.find(node => node.id === map.activeNodeId) ?? map.nodes.find(node => node.id === 'svc-checkout') ?? map.nodes[0];
+    setTopologySearchQuery(routeContext?.search);
+  }, [routeContext?.search, routeContextKey]);
+  const topologyRoutePrimaryNode =
+    topologyVisibleActiveNodeId
+      ? map.nodes.find(node => node.id === topologyVisibleActiveNodeId)
+      : topologyCanShowApiFocus
+        ? map.nodes.find(node => node.id === 'svc-checkout') ?? map.nodes[0]
+        : undefined;
   const topologyLocalSelectedNode = topologyLocalSelection.nodeId ? findNode(map.nodes, topologyLocalSelection.nodeId) : undefined;
   const primaryNode = topologyLocalSelectedNode ?? topologyRoutePrimaryNode;
   const activeViewModeLabel = map.viewModes.find(mode => mode.active)?.label;
@@ -795,12 +1030,15 @@ export default function TopologyPage({
     map.filterContext.groupBy === 'source-kind'
       ? Object.fromEntries(map.sources.map(source => [source.kind, withTopologyGroupByHref(source.href, 'source-kind')]))
       : {};
+  const topologyRouteDepth = Number(routeContext?.depth);
+  const topologyRouteDepthValue = Number.isFinite(topologyRouteDepth) ? Math.max(0, Math.floor(topologyRouteDepth)) : undefined;
+  const topologyScopedDepth = topologyRouteDepthValue ?? map.apiDepth;
   const topologyG6ResetParams = React.useMemo(() => {
     const params = new URLSearchParams();
     params.set('environment', map.filterContext.environment);
     params.set('timeRange', map.filterContext.timeRange);
     params.set('viewMode', map.filterContext.viewMode);
-    if (typeof map.apiDepth === 'number') params.set('depth', String(map.apiDepth));
+    if (typeof topologyScopedDepth === 'number') params.set('depth', String(topologyScopedDepth));
     appendTopologySelectionParam(params, 'start', routeContext?.start);
     appendTopologySelectionParam(params, 'end', routeContext?.end);
     appendTopologySelectionParam(params, 'refresh', routeContext?.refresh);
@@ -809,10 +1047,9 @@ export default function TopologyPage({
     appendTopologySelectionParam(params, 'relationType', routeContext?.relationType);
     appendTopologySelectionParam(params, 'hideInternal', routeContext?.hideInternal);
     if (map.filterContext.sourceKind) params.set('sourceKind', map.filterContext.sourceKind);
-    if (map.filterContext.groupBy !== 'none') params.set('groupBy', map.filterContext.groupBy);
+    if (topologyHasRenderableGraph && map.filterContext.groupBy !== 'none') params.set('groupBy', map.filterContext.groupBy);
     return params;
   }, [
-    map.apiDepth,
     map.filterContext.environment,
     map.filterContext.groupBy,
     map.filterContext.sourceKind,
@@ -824,16 +1061,19 @@ export default function TopologyPage({
     routeContext?.refresh,
     routeContext?.relationType,
     routeContext?.start,
-    routeContext?.tz
+    routeContext?.tz,
+    topologyHasRenderableGraph,
+    topologyScopedDepth
   ]);
   const topologyG6FilterResetParams = new URLSearchParams(topologyG6ResetParams);
   topologyG6FilterResetParams.delete('groupBy');
   const topologyG6ResetHref = `/topology?${topologyG6FilterResetParams.toString()}`;
+  const topologyGlobalScopeHref = `/topology?${topologyG6ResetParams.toString()}`;
   const topologyFocusExitParams = new URLSearchParams();
   topologyFocusExitParams.set('environment', map.filterContext.environment);
   topologyFocusExitParams.set('timeRange', map.filterContext.timeRange);
   topologyFocusExitParams.set('viewMode', map.filterContext.viewMode);
-  topologyFocusExitParams.set('depth', '2');
+  topologyFocusExitParams.set('depth', String(topologyScopedDepth ?? 2));
   appendTopologySelectionParam(topologyFocusExitParams, 'start', routeContext?.start);
   appendTopologySelectionParam(topologyFocusExitParams, 'end', routeContext?.end);
   appendTopologySelectionParam(topologyFocusExitParams, 'refresh', routeContext?.refresh);
@@ -843,13 +1083,13 @@ export default function TopologyPage({
   appendTopologySelectionParam(topologyFocusExitParams, 'hideInternal', routeContext?.hideInternal);
   appendTopologySelectionParam(topologyFocusExitParams, 'edgeId', map.selectedEdgeId ?? routeContext?.edgeId);
   if (map.filterContext.sourceKind) topologyFocusExitParams.set('sourceKind', map.filterContext.sourceKind);
-  if (map.filterContext.groupBy !== 'none') topologyFocusExitParams.set('groupBy', map.filterContext.groupBy);
+  if (topologyHasRenderableGraph && map.filterContext.groupBy !== 'none') topologyFocusExitParams.set('groupBy', map.filterContext.groupBy);
   const topologyFocusExitHref = `/topology?${topologyFocusExitParams.toString()}`;
   const topologyG6GroupParams = new URLSearchParams(topologyG6ResetParams);
   topologyG6GroupParams.set('groupBy', 'source-kind');
   const topologyG6SearchParams = new URLSearchParams(topologyG6ResetParams);
   if (topologyToolbarSearchQuery) topologyG6SearchParams.set('search', topologyToolbarSearchQuery);
-  if (map.filterContext.groupBy !== 'none') topologyG6SearchParams.set('groupBy', map.filterContext.groupBy);
+  if (topologyHasRenderableGraph && map.filterContext.groupBy !== 'none') topologyG6SearchParams.set('groupBy', map.filterContext.groupBy);
   const handleTopologyEnvironmentChange = React.useCallback(
     (value: string) => {
       navigateTopologyFocus(buildTopologyScopeHref(topologyG6ResetParams, { environment: value }), topologyRouter.push);
@@ -884,44 +1124,54 @@ export default function TopologyPage({
     },
     [topologyRouter.push]
   );
+  const handleTopologyGlobalScope = React.useCallback(
+    (event: React.MouseEvent<HTMLAnchorElement>) => {
+      event.preventDefault();
+      navigateTopologyFocus(topologyGlobalScopeHref, topologyRouter.push);
+    },
+    [topologyGlobalScopeHref, topologyRouter.push]
+  );
   const topologyToolbarSummaryItems = [
-    topologyEffectiveSearchQuery || t('topology.all-entities'),
+    topologyVisibleFocusQuery || t('topology.all-entities'),
     map.filterContext.environment,
     map.filterContext.timeRange,
     activeViewModeLabel,
     activeSourceLabel
   ].filter((item): item is string => Boolean(item));
-  const topologyFocusState = map.activeNodeId ?? (topologyEffectiveSearchQuery ? 'incoming-context' : 'all');
-  const routeFocusDepth = Number(routeContext?.depth);
-  const topologyFocusTrailDepth = Number.isFinite(routeFocusDepth) ? Math.max(0, Math.floor(routeFocusDepth)) : map.apiDepth ?? 2;
-  const topologyFocusTrailMode = primaryNode || topologyEffectiveSearchQuery || map.apiDepth === 1 ? 'focused' : 'overview';
+  const topologyFocusState = topologyVisibleActiveNodeId ?? (topologyVisibleFocusQuery ? 'incoming-context' : 'all');
+  const topologyFocusTrailDepth = topologyScopedDepth ?? 2;
+  const topologyFocusTrailMode = primaryNode || topologyVisibleFocusQuery || map.apiDepth === 1 ? 'focused' : 'overview';
   const topologyFocusTrailEntityId = primaryNode?.id ?? topologyFocusState;
+  const topologyShouldShowFocusExit = topologyFocusTrailMode === 'focused' && topologyFocusTrailEntityId !== 'all';
   const topologyToolbarStateItems: HzTopologyToolbarStateItem[] = [
     {
       id: 'focus',
       label: t('topology.state.focus'),
-      value: (primaryNode?.label ?? topologyEffectiveSearchQuery) || t('topology.all-entities'),
+      value: (primaryNode?.label ?? topologyVisibleFocusQuery) || t('topology.all-entities'),
       tone: primaryNode ? topologyNodeHealthTone(primaryNode) : 'neutral',
       'data-topology-focus-state': topologyFocusState
     },
     {
       id: 'depth',
       label: t('topology.state.depth'),
-      value: formatTopologyDepth(map.apiDepth, t),
-      'data-topology-depth-state': map.apiDepth ?? 'none'
-    },
-    {
+      value: formatTopologyDepth(topologyScopedDepth, t),
+      'data-topology-depth-state': topologyScopedDepth ?? 'none'
+    }
+  ];
+  if (topologyHasRenderableGraph) {
+    topologyToolbarStateItems.push({
       id: 'group',
       label: t('topology.state.group'),
       value: t(`topology.state.group.${map.filterContext.groupBy}`),
       'data-topology-group-state': map.filterContext.groupBy
-    }
-  ];
+    });
+  }
   const topologyApiNodeCount = loadedApiGraph?.nodes?.length ?? map.nodes.length;
-  const topologyHiddenCount = Math.max(0, topologyApiNodeCount - map.nodes.length);
+  const topologyHiddenCount = Math.max(0, topologyApiNodeCount - topologyCanvasNodes.length);
+  const topologyToolbarGroupControlVisibility = topologyHasRenderableGraph ? 'visible' : 'hidden-empty-graph';
   const topologyGroupPanelItems = buildTopologyGroupPanelItems({
-    nodes: map.nodes,
-    edges: map.edges,
+    nodes: topologyCanvasNodes,
+    edges: topologyCanvasEdges,
     activeSourceLabel,
     primaryNode,
     hiddenCount: topologyHiddenCount,
@@ -939,15 +1189,15 @@ export default function TopologyPage({
           id: 'active-entity',
           href: primaryNode.links.entityHref,
           label: primaryNode.label,
-          value: formatTopologyDepth(map.apiDepth, t),
+          value: formatTopologyDepth(topologyScopedDepth, t),
           active: true,
           'data-topology-focus-crumb': primaryNode.id
         }
       : {
           id: 'active-entity',
           href: '/topology',
-          label: topologyEffectiveSearchQuery || t('topology.all-entities'),
-          value: formatTopologyDepth(map.apiDepth, t),
+          label: topologyVisibleFocusQuery || t('topology.all-entities'),
+          value: formatTopologyDepth(topologyScopedDepth, t),
           active: true,
           'data-topology-focus-crumb': topologyFocusState
         }
@@ -978,7 +1228,7 @@ export default function TopologyPage({
       'data-topology-focus-filter-view': map.filterContext.viewMode
     }
   ];
-  const topologyMetricRows = React.useMemo(() => buildTopologyMetricRows(map.edges, map.nodes), [map.edges, map.nodes]);
+  const topologyMetricRows = React.useMemo(() => buildTopologyMetricRows(topologyCanvasEdges, map.nodes), [map.nodes, topologyCanvasEdges]);
   const [topologyMetricWindowFilter, setTopologyMetricWindowFilter] = React.useState<HzTopologyMetricTableRenderWindowFilter>('all');
   React.useEffect(() => {
     setTopologyMetricWindowFilter('all');
@@ -997,10 +1247,10 @@ export default function TopologyPage({
     setTopologyCompanionCollapsedSections(current => ({ ...current, [sectionId]: collapsed }));
   }, []);
   const topologyLocalSelectedEdge = topologyLocalSelection.edgeId ? findEdge(map.edges, topologyLocalSelection.edgeId) : undefined;
-  const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map);
+  const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map, topologyCanvasEdges);
   const topologyCanvasSelectedEdgeId = topologyLocalSelection.nodeId
     ? undefined
-    : topologyLocalSelectedEdge?.id ?? map.selectedEdgeId ?? topologyDetailEdge?.id;
+    : topologyLocalSelectedEdge?.id ?? map.selectedEdgeId;
   const topologyMetricSelectedRowId = topologyLocalSelection.nodeId
     ? undefined
     : map.selectedEdgeId ?? topologyDetailEdge?.id ?? topologyMetricRows[0]?.id;
@@ -1017,18 +1267,43 @@ export default function TopologyPage({
     const strategy = buildHzTopologyG6LargeGraphStrategy(topologyG6Graph);
     return buildHzTopologyG6RenderWindow(topologyG6Graph, strategy, { priorityNodeIds: topologyRenderWindowPriorityNodeIds });
   }, [topologyG6Graph, topologyRenderWindowPriorityNodeIds]);
+  const topologyG6RenderedEdgeCount = topologyRenderWindowCompanion.mode === 'windowed'
+    ? Math.min(topologyRenderWindowCompanion.renderedEdgeCount, HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT)
+    : topologyG6Graph.edges.length;
+  const topologyG6HiddenEdgeCount = Math.max(0, topologyG6Graph.edges.length - topologyG6RenderedEdgeCount);
+  const topologyG6SummaryLabel = topologyRenderWindowCompanion.mode === 'windowed'
+    ? `${t('topology.g6.summary.rendered-node-count', {
+        rendered: topologyRenderWindowCompanion.renderedNodeCount,
+        total: topologyG6Graph.nodes.length
+      })} · ${t('topology.g6.summary.rendered-edge-count', {
+        rendered: topologyG6RenderedEdgeCount,
+        total: topologyG6Graph.edges.length
+      })}`
+    : `${t('topology.group-panel.node-count', { count: topologyG6Graph.nodes.length })} · ${t('topology.group-panel.edge-count', { count: topologyG6Graph.edges.length })}`;
+  const topologyG6SummaryCountPolicy = topologyRenderWindowCompanion.mode === 'windowed' ? 'windowed-rendered-vs-total' : 'direct-total';
   const topologyMetricRenderWindowCompanion = React.useMemo(() => ({
     mode: topologyRenderWindowCompanion.mode,
     totalNodeCount: topologyRenderWindowCompanion.totalNodeCount,
     renderedNodeCount: topologyRenderWindowCompanion.renderedNodeCount,
     hiddenNodeCount: topologyRenderWindowCompanion.hiddenNodeCount,
+    totalEdgeCount: topologyG6Graph.edges.length,
+    renderedEdgeCount: topologyG6RenderedEdgeCount,
     visibleNodeBudget: topologyRenderWindowCompanion.visibleNodeBudget,
     tableCompanion: topologyRenderWindowCompanion.tableCompanion,
     priorityNodeIds: topologyRenderWindowCompanion.priorityNodeIds,
     renderedNodeIds: topologyRenderWindowCompanion.graph.nodes.map(node => node.id)
-  }), [topologyRenderWindowCompanion]);
+  }), [topologyG6Graph.edges.length, topologyG6RenderedEdgeCount, topologyRenderWindowCompanion]);
+  const topologyDetailEdgeRenderWindowContext = React.useMemo(
+    () => buildTopologyEdgeRenderWindowContext(
+      topologyDetailEdge,
+      topologyMetricRenderWindowCompanion.mode,
+      topologyMetricRenderWindowCompanion.renderedNodeIds
+    ),
+    [topologyDetailEdge, topologyMetricRenderWindowCompanion.mode, topologyMetricRenderWindowCompanion.renderedNodeIds]
+  );
   const topologyShouldShowRenderWindowMetricTable =
     topologyRenderWindowCompanion.mode === 'windowed' || topologyRenderWindowCompanion.tableCompanion === 'required';
+  const topologyShouldRenderCompanionMetricTable = !topologyShouldShowRenderWindowMetricTable;
   const topologyMetricTableLabels = React.useMemo(() => ({
     edgeCount: t('topology.metric-table.edge-count', { count: topologyMetricRows.length }),
     requestRate: t('topology.metric-table.request-rate-unit'),
@@ -1040,13 +1315,30 @@ export default function TopologyPage({
     renderWindowFilterPartial: t('topology.metric-table.filter.partial'),
     renderWindowFilterHidden: t('topology.metric-table.filter.hidden'),
     renderWindowFilterUnknown: t('topology.metric-table.filter.unknown'),
+    renderWindowEdgeSummary: t('topology.metric-table.edge-summary', {
+      rendered: topologyG6RenderedEdgeCount,
+      total: topologyG6Graph.edges.length
+    }),
+    renderWindowRowSummary: (rendered: number, total: number) => t('topology.metric-table.row-render-summary', { rendered, total }),
+    renderWindowShowMore: (next: number, total: number) => t('topology.metric-table.row-render-show-more', { next, total }),
     rowAriaLabel: (row: HzTopologyMetricRow) => t('topology.metric-table.open-edge-aria', { edge: String(row.id) })
-  }), [t, topologyMetricRows.length]);
+  }), [t, topologyG6Graph.edges.length, topologyG6RenderedEdgeCount, topologyMetricRows.length]);
   const topologyHoveredDetailEdge = topologyG6HoveredEdgeId ? findEdge(map.edges, topologyG6HoveredEdgeId) : undefined;
   const topologyLiveHoverEdge = topologyHoveredDetailEdge;
-  const topologyInvestigationEdge = topologyHoveredDetailEdge ?? topologyDetailEdge;
+  const topologySelectedPathSummaryEdge = topologyCanvasSelectedEdgeId ? findEdge(map.edges, topologyCanvasSelectedEdgeId) : undefined;
+  const topologyInvestigationEdge = topologySelectedPathSummaryEdge ?? topologyHoveredDetailEdge ?? topologyDetailEdge;
   const topologyHoverFromNode = topologyInvestigationEdge ? findNode(map.nodes, topologyInvestigationEdge.from) : undefined;
   const topologyHoverToNode = topologyInvestigationEdge ? findNode(map.nodes, topologyInvestigationEdge.to) : undefined;
+  const topologyPathSummaryInteractionState = topologyCanvasSelectedEdgeId
+    ? 'selected'
+    : topologyG6HoveredEdgeId
+      ? 'hovered'
+      : 'preview';
+  const topologyPathSummaryTitle = topologyPathSummaryInteractionState === 'selected'
+    ? t('topology.path-summary.title.selected')
+    : topologyPathSummaryInteractionState === 'hovered'
+      ? t('topology.path-summary.title.hovered')
+      : t('topology.path-summary.title.preview');
   const topologyLiveHoverFromNode = topologyLiveHoverEdge ? findNode(map.nodes, topologyLiveHoverEdge.from) : undefined;
   const topologyLiveHoverToNode = topologyLiveHoverEdge ? findNode(map.nodes, topologyLiveHoverEdge.to) : undefined;
   const topologyCanvasDrawerMode: HzTopologyCanvasDrawerMode = topologyDetailEdge && primaryNode
@@ -1056,7 +1348,18 @@ export default function TopologyPage({
       : primaryNode
         ? 'node'
         : 'none';
+  const topologyShouldShowCompanionRail = Boolean(primaryNode || topologyDetailEdge);
+  const topologyWorkbenchLayout = topologyShouldShowCompanionRail ? 'canvas-companion' : 'canvas-only';
+  const topologyCompanionRailVisibility = topologyShouldShowCompanionRail ? 'visible-side-rail' : 'hidden-from-layout';
   const topologyCompanionJumpActiveId = topologyDetailEdge ? 'edge-detail' : primaryNode ? 'current-node' : 'view-mode';
+  const topologyCompanionJumpActiveResetKey = [
+    topologyCompanionJumpActiveId,
+    primaryNode?.id ?? 'none',
+    topologyDetailEdge?.id ?? 'none'
+  ].join(':');
+  const topologyCompanionSelectionResetKey = topologyLocalSelection.source === 'none'
+    ? undefined
+    : topologyCompanionJumpActiveResetKey;
   const topologyCompanionJumpItems = [
     {
       id: 'view-mode',
@@ -1070,12 +1373,16 @@ export default function TopologyPage({
       label: t('topology.companion.jump.legend'),
       active: topologyCompanionJumpActiveId === 'legend'
     },
-    {
-      id: 'edge-red',
-      href: '#topology-companion-edge-red',
-      label: t('topology.companion.jump.red'),
-      active: topologyCompanionJumpActiveId === 'edge-red'
-    },
+    ...(topologyShouldRenderCompanionMetricTable
+      ? [
+          {
+            id: 'edge-red',
+            href: '#topology-companion-edge-red',
+            label: t('topology.companion.jump.red'),
+            active: topologyCompanionJumpActiveId === 'edge-red'
+          }
+        ]
+      : []),
     ...(map.impactTimeline.length > 0
       ? [
           {
@@ -1117,8 +1424,29 @@ export default function TopologyPage({
   const topologyLoadingEvidenceSources = ['api', 'greptime', 'trace', 'relation'];
   const topologyTraceCallUnavailable = loadedApiGraph === null && topologyTraceCallScope;
   const topologyShouldShowEmptyState =
-    map.dataSource === 'api' && !topologyIsPending && (map.nodes.length === 0 || topologyTraceCallMissingEdges);
-  const topologyEmptyStateKind = loadedApiGraph === null ? 'degraded' : topologyTraceCallMissingEdges ? 'filtered-empty' : 'api-empty';
+    map.dataSource === 'api' && !topologyIsPending && (map.nodes.length === 0 || topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing);
+  const topologyRelationGapApiSourceKinds =
+    loadedApiGraph?.sourceKinds && loadedApiGraph.sourceKinds.length > 0
+      ? loadedApiGraph.sourceKinds.join(' ')
+      : 'none';
+  const topologyRelationGapTraceCallParams = new URLSearchParams(topologyG6ResetParams);
+  topologyRelationGapTraceCallParams.set('viewMode', 'service-call');
+  topologyRelationGapTraceCallParams.set('sourceKind', 'otlp-trace-call');
+  topologyRelationGapTraceCallParams.set('relationType', 'trace-call');
+  const topologyRelationGapTraceCallHref = `/topology?${topologyRelationGapTraceCallParams.toString()}`;
+  const handleTopologyRelationGapTraceCall = React.useCallback(
+    (event: React.MouseEvent<HTMLAnchorElement>) => {
+      event.preventDefault();
+      topologyRouter.push(topologyRelationGapTraceCallHref);
+    },
+    [topologyRelationGapTraceCallHref, topologyRouter]
+  );
+  const topologyEmptyStateKind =
+    loadedApiGraph === null
+      ? 'degraded'
+      : topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing
+        ? 'filtered-empty'
+        : 'api-empty';
   const topologyEmptyStateDataKind = topologyTraceCallUnavailable
     ? 'trace-call-degraded'
     : topologyTraceCallMissingEdges
@@ -1131,6 +1459,8 @@ export default function TopologyPage({
       ? ['api', 'unavailable']
       : topologyTraceCallMissingEdges
         ? ['otlp-trace-call', 'greptime', 'trace']
+        : topologyFilteredEvidenceMissing
+          ? [map.filterContext.sourceKind ?? map.filterContext.viewMode]
         : topologyEvidenceSources;
   const topologyEmptyStateTitle = topologyTraceCallMissingEdges
     ? t('topology.empty.trace-call.title')
@@ -1152,7 +1482,9 @@ export default function TopologyPage({
       ? t('topology.degraded.trace-call.source')
     : topologyEmptyStateKind === 'degraded'
       ? t('topology.degraded.api.source')
-      : 'API';
+      : topologyFilteredEvidenceMissing
+        ? (activeSourceLabel ?? activeViewModeLabel ?? 'API')
+        : 'API';
   const topologyLegendNodeTypeItems = React.useMemo(
     () => {
       const visibleNodeKinds = new Set(
@@ -1163,7 +1495,6 @@ export default function TopologyPage({
       return HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => visibleNodeKinds.has(icon.kind)).map(icon => ({
         id: `node-type-${icon.kind}`,
         label: t(`topology.legend.node-type.${icon.kind}`),
-        value: icon.iconName,
         iconSrc: icon.iconSrc,
         iconAlt: '',
         iconLibrary: icon.iconLibrary,
@@ -1174,80 +1505,18 @@ export default function TopologyPage({
     },
     [topologyG6Graph.nodes, t]
   );
-  const topologyHasVisibleNodes = topologyG6Graph.nodes.length > 0;
-  const topologyHasVisibleGraphEvidence = topologyHasVisibleNodes || topologyG6Graph.edges.length > 0;
   const topologyLegendSections = React.useMemo(
     () => {
-      const sections = [
+      const sections: HzTopologyLegendSection[] = [
         {
           id: 'node-type',
           label: t('topology.legend.node-type'),
           items: topologyLegendNodeTypeItems
         }
       ];
-      if (topologyHasVisibleNodes) {
-        sections.push({
-          id: 'status',
-          label: t('topology.legend.status'),
-          items: [
-            {
-              id: 'healthy-node',
-              label: t('topology.legend.status.healthy'),
-              color: '#22c55e',
-              visualSource: 'hertzbeat-status-token' as const,
-              value: t('topology.legend.status.healthy-value')
-            },
-            {
-              id: 'warning-node',
-              label: t('topology.legend.status.warning'),
-              color: '#f59e0b',
-              visualSource: 'hertzbeat-status-token' as const,
-              value: t('topology.legend.status.warning-value')
-            },
-            {
-              id: 'critical-node',
-              label: t('topology.legend.status.critical'),
-              color: '#ef4444',
-              visualSource: 'hertzbeat-status-token' as const,
-              value: t('topology.legend.status.critical-value')
-            }
-          ]
-        });
-      }
-      if (topologyHasVisibleGraphEvidence) {
-        sections.push({
-          id: 'interaction',
-          label: t('topology.legend.interaction'),
-          items: [
-            {
-              id: 'selected-node',
-              label: t('topology.legend.interaction.selected-node'),
-              color: '#e5edf8',
-              visualSource: 'hertzbeat-interaction-token' as const,
-              value: t('topology.legend.interaction.selected-node-value')
-            },
-            {
-              id: 'directional-edge',
-              label: t('topology.legend.interaction.directional-edge'),
-              color: '#94a3b8',
-              pattern: 'solid' as const,
-              visualSource: 'hertzbeat-edge-token' as const,
-              value: t('topology.legend.interaction.directional-edge-value')
-            },
-            {
-              id: 'dimmed-edge',
-              label: t('topology.legend.interaction.dimmed-edge'),
-              color: '#94a3b8',
-              pattern: 'muted' as const,
-              visualSource: 'hertzbeat-edge-token' as const,
-              value: t('topology.legend.interaction.dimmed-edge-value')
-            }
-          ]
-        });
-      }
       return sections;
     },
-    [t, topologyHasVisibleGraphEvidence, topologyHasVisibleNodes, topologyLegendNodeTypeItems]
+    [t, topologyLegendNodeTypeItems]
   );
   const topologyEdgeIds = React.useMemo(
     () => new Set(map.edges.map(edge => edge.id)),
@@ -1325,14 +1594,21 @@ export default function TopologyPage({
     <HzTopologyWorkbenchFrame
       as="main"
       data-topology-route="hertzbeat-entity-topology"
+      data-hz-topology-frame="workbench"
       data-topology-data-source={map.dataSource}
       data-topology-incoming-context={map.filterContext.hasIncomingContext ? 'entity-filter' : 'none'}
-      data-topology-active-node-id={map.activeNodeId ?? 'none'}
+      data-topology-active-node-id={topologyVisibleActiveNodeId ?? 'none'}
       data-topology-active-edge-id={map.selectedEdgeId ?? 'none'}
       data-topology-selection-behavior="in-page-drawer"
       data-topology-selection-source={topologyLocalSelection.source}
       data-topology-selection-node-id={topologyLocalSelection.nodeId ?? 'none'}
       data-topology-selection-edge-id={topologyLocalSelection.edgeId ?? 'none'}
+      data-topology-live-interaction-proof="wheel-hover-click-preserve-url-viewport"
+      data-topology-live-interaction-proof-owner="hertzbeat-ui-g6-live-interaction"
+      data-topology-normal-browser-giant-node-proof="compact-1x-no-fill"
+      data-topology-normal-browser-giant-node-proof-owner="hertzbeat-ui-g6-viewport"
+      data-topology-ordinary-inspection-proof="api-current-window-no-stale-empty-gap"
+      data-topology-ordinary-inspection-proof-owner="hertzbeat-ui-workbench-frame"
       data-topology-focus-navigation="explicit-soft-route"
       data-topology-focus-navigation-owner="hertzbeat-ui-g6-focus-entry"
       data-topology-focus-navigation-trigger="double-click-node"
@@ -1350,6 +1626,13 @@ export default function TopologyPage({
       data-topology-search-navigation-url-policy="preserve-current-url"
       data-topology-active-view-mode={map.filterContext.viewMode}
       data-topology-active-source-kind={map.filterContext.sourceKind ?? 'all'}
+      data-topology-api-settle-state={topologyApiSettleState}
+      data-hz-topology-api-state={topologyApiSettleState}
+      data-topology-api-settle-state-owner="hertzbeat-ui-workbench-frame"
+      data-topology-api-error-state={topologyApiErrorState}
+      data-topology-api-error-status={topologyApiErrorStatus}
+      data-topology-api-error-resolution={topologyApiErrorResolution}
+      data-topology-api-error-owner="hertzbeat-ui-workbench-frame"
       data-topology-trace-call-state={topologyTraceCallState}
       data-topology-trace-call-proof-owner="hertzbeat-ui-workbench-frame"
       data-topology-trace-call-edge-count={topologyTraceCallEdges.length}
@@ -1357,13 +1640,32 @@ export default function TopologyPage({
       data-topology-trace-call-window-edge-count={topologyTraceCallWindowEdgeCount}
       data-topology-trace-call-sample-edge-count={topologyTraceCallSampleEdgeCount}
       data-topology-trace-call-red-state={topologyTraceCallRedState}
+      data-topology-relation-gap-state={topologyRelationGapState}
+      data-topology-relation-gap-owner="hertzbeat-ui-relation-gap"
+      data-topology-relation-gap-node-count={map.nodes.length}
+      data-topology-relation-gap-edge-count={map.edges.length}
+      data-topology-relation-gap-source-kind={map.filterContext.sourceKind ?? 'all'}
+      data-topology-relation-gap-api-source-kinds={topologyRelationGapApiSourceKinds}
+      data-topology-relation-gap-view-mode={map.filterContext.viewMode}
+      data-topology-relation-gap-visual={topologyRelationGapVisual}
+      data-topology-relation-gap-canvas-policy={topologyRelationGapCanvasPolicy}
+      data-topology-empty-state={topologyShouldShowEmptyState ? topologyEmptyStateDataKind : 'none'}
       data-topology-api-depth={map.apiDepth ?? 'none'}
       data-topology-api-scope-owner="hertzbeat-ui-workbench-frame"
       data-topology-api-request-path={topologyApiRequestPath}
       data-topology-api-scope-source-kind={map.filterContext.sourceKind ?? 'all'}
       data-topology-api-scope-relation-type={topologyApiScopeRelationType}
       data-topology-api-scope-view-mode={map.filterContext.viewMode}
+      data-topology-api-focus-resolution={topologyHasUnresolvableFocusEntity ? 'unresolvable-client-focus' : 'api-compatible'}
+      data-topology-api-focus-resolution-owner="topology-route-api-scope"
       data-topology-api-timeout-ms={topologyApiRequestTimeoutMs}
+      data-topology-scale-proof={topologyScaleProof || undefined}
+      data-topology-scale-proof-owner={topologyScaleProof ? 'topology-route-api-scope' : undefined}
+      data-topology-scale-proof-focus-entity-id={topologyScaleProofFocusEntityId}
+      data-topology-scale-proof-api-policy={topologyScaleProofApiPolicy}
+      data-topology-scale-proof-time-policy={topologyScaleProofTimeWindow?.policy}
+      data-topology-scale-proof-window-start={topologyScaleProofTimeWindow?.start}
+      data-topology-scale-proof-window-end={topologyScaleProofTimeWindow?.end}
       data-topology-workbench-frame-owner="hertzbeat-ui-workbench-frame"
       data-topology-workbench-frame-boundary-owner="hertzbeat-ui-workbench-frame-boundary"
       density="compact"
@@ -1400,7 +1702,9 @@ export default function TopologyPage({
               {
                 id: 'refresh',
                 label: t('topology.refresh'),
-                'data-topology-refresh-action': 'refresh'
+                onClick: handleTopologyRefresh,
+                'data-topology-refresh-action': 'refresh',
+                'data-topology-refresh-action-behavior': 'in-page-api-reload'
               }
             ]}
           />
@@ -1418,25 +1722,46 @@ export default function TopologyPage({
         label={t('topology.focus-trail.label')}
         crumbs={topologyFocusTrailCrumbs}
         filters={topologyFocusTrailFilters}
-        hiddenCountLabel={t('topology.focus-trail.hidden-count', { count: topologyHiddenCount })}
+        hiddenCountLabel={topologyHiddenCount > 0 ? t('topology.focus-trail.hidden-count', { count: topologyHiddenCount }) : undefined}
         hiddenCountProps={{
           'data-topology-focus-trail-hidden-count-owner': 'hertzbeat-ui-focus-trail-hidden-count'
         } as React.HTMLAttributes<HTMLSpanElement>}
-        exitAction={{
-          href: topologyFocusExitHref,
-          label: t('topology.focus-trail.exit'),
-          onClick: handleTopologyFocusExit,
-          'data-topology-focus-trail-exit-owner': 'hertzbeat-ui-focus-trail-exit'
-        } as React.AnchorHTMLAttributes<HTMLAnchorElement> & { label: React.ReactNode }}
+        exitAction={
+          topologyShouldShowFocusExit
+            ? ({
+                href: topologyFocusExitHref,
+                label: t('topology.focus-trail.exit'),
+                onClick: handleTopologyFocusExit,
+                'data-topology-focus-trail-exit-owner': 'hertzbeat-ui-focus-trail-exit'
+              } as React.AnchorHTMLAttributes<HTMLAnchorElement> & { label: React.ReactNode })
+            : undefined
+        }
         boundary="none"
         density="graph-dock"
       />
 
+      {topologyHasRouteEntityFocus ? (
+        <HzTopologyActionLink
+          data-topology-view-scope="focused-adjacency"
+          data-topology-view-scope-owner="hertzbeat-ui-focus-scope-guide"
+          data-topology-view-scope-global-action="open-global-topology"
+          data-topology-view-scope-global-href={topologyGlobalScopeHref}
+          id="focused-adjacency-global-scope"
+          href={topologyGlobalScopeHref}
+          label={t('topology.view-scope.focused-adjacency')}
+          copy={t('topology.view-scope.focused-adjacency.copy')}
+          spacing="none"
+          onClick={handleTopologyGlobalScope}
+          className="mx-0 -mt-1 mb-1 w-full border-transparent bg-transparent px-0 py-0 text-[11px] text-[#9ca3b4] hover:border-transparent hover:bg-transparent"
+        />
+      ) : null}
+
       <HzTopologyToolbar
         data-topology-controls="hertzbeat-topology-controls"
         data-topology-controls-owner="hertzbeat-ui-toolbar"
         data-topology-toolbar-boundary-owner="hertzbeat-ui-toolbar-boundary"
         data-topology-toolbar-state-owner="hertzbeat-ui-toolbar-state"
+        data-topology-toolbar-group-control-visibility={topologyToolbarGroupControlVisibility}
         data-topology-filter-summary="incoming-context"
         density="graph-first"
         boundary="none"
@@ -1457,7 +1782,7 @@ export default function TopologyPage({
           ...map.sources.map(source => ({ value: source.kind, label: source.label }))
         ]}
         depthLabel={t('topology.state.depth')}
-        depthValue={String(map.apiDepth ?? 2)}
+        depthValue={String(topologyScopedDepth ?? 2)}
         depthOptions={[
           { value: '1', label: t('topology.state.depth.one-hop') },
           { value: '2', label: t('topology.state.depth.two-hop') },
@@ -1465,12 +1790,16 @@ export default function TopologyPage({
         ]}
         groupByLabel={t('topology.state.group')}
         groupByValue={map.filterContext.groupBy}
-        groupByOptions={[
-          { value: 'none', label: t('topology.state.group.none') },
-          { value: 'environment', label: t('topology.state.group.environment') },
-          { value: 'source-kind', label: t('topology.state.group.source-kind') },
-          { value: 'entity-type', label: t('topology.state.group.entity-type') }
-        ]}
+        groupByOptions={
+          topologyHasRenderableGraph
+            ? [
+                { value: 'none', label: t('topology.state.group.none') },
+                { value: 'environment', label: t('topology.state.group.environment') },
+                { value: 'source-kind', label: t('topology.state.group.source-kind') },
+                { value: 'entity-type', label: t('topology.state.group.entity-type') }
+              ]
+            : []
+        }
         resetLabel={t('topology.toolbar.reset')}
         resetHref="/topology"
         onEnvironmentChange={handleTopologyEnvironmentChange}
@@ -1485,7 +1814,7 @@ export default function TopologyPage({
         stateItems={topologyToolbarStateItems}
       />
 
-      <HzTopologyWorkbenchGrid data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid" layout="canvas-only">
+      <HzTopologyWorkbenchGrid data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid" layout={topologyWorkbenchLayout}>
         <HzTopologyWorkbenchSlot
           data-topology-canvas-slot-owner="hertzbeat-ui-workbench-slot"
           kind="canvas"
@@ -1497,6 +1826,15 @@ export default function TopologyPage({
             data-topology-canvas-interaction-owner="hertzbeat-ui-canvas-interaction"
             data-topology-canvas-interaction-scope-owner="hertzbeat-ui-canvas-interaction-scope"
             data-topology-canvas-boundary-owner="hertzbeat-ui-canvas-boundary"
+            data-topology-g6-render-proof-owner="hertzbeat-topology-page-g6-proof"
+            data-topology-g6-render-window-mode={topologyRenderWindowCompanion.mode}
+            data-topology-g6-render-window-total-node-count={topologyRenderWindowCompanion.totalNodeCount}
+            data-topology-g6-render-window-rendered-node-count={topologyRenderWindowCompanion.renderedNodeCount}
+            data-topology-g6-render-window-hidden-node-count={topologyRenderWindowCompanion.hiddenNodeCount}
+            data-topology-g6-render-window-total-edge-count={topologyRenderWindowCompanion.totalEdgeCount}
+            data-topology-g6-render-window-rendered-edge-count={topologyG6RenderedEdgeCount}
+            data-topology-g6-render-window-hidden-edge-count={topologyG6HiddenEdgeCount}
+            data-topology-g6-summary-count-policy={topologyG6SummaryCountPolicy}
             layout={topologyLayout}
             interactionMode="inspect"
             interactionScope="hover-group"
@@ -1509,9 +1847,44 @@ export default function TopologyPage({
           <HzTopologyCanvasAnnotation
             data-topology-canvas-annotation-owner="hertzbeat-ui-canvas-annotation"
             title={topologyLayoutLabel}
-            copy={`${formatTopologyDepth(map.apiDepth, t)} · ${formatTimeRange(map.filterContext.timeRange, t)}`}
+            copy={`${formatTopologyDepth(topologyScopedDepth, t)} · ${formatTimeRange(map.filterContext.timeRange, t)}`}
             visibility="assistive"
           />
+          {topologyRelationGap ? (
+            <HzTopologyCanvasAnnotation
+              data-topology-relation-gap-state="nodes-without-edges"
+              data-topology-relation-gap-owner="hertzbeat-ui-relation-gap"
+              data-topology-relation-gap-node-count={map.nodes.length}
+              data-topology-relation-gap-edge-count={map.edges.length}
+              data-topology-relation-gap-source-kind={map.filterContext.sourceKind ?? 'all'}
+              data-topology-relation-gap-api-source-kinds={topologyRelationGapApiSourceKinds}
+              data-topology-relation-gap-view-mode={map.filterContext.viewMode}
+              data-topology-relation-gap-visual="canvas-annotation"
+              data-topology-relation-gap-canvas-policy="hide-isolated-node-only-graph"
+              title={t('topology.relation-gap.title')}
+              copy={t('topology.relation-gap.copy')}
+              placement="top-left"
+              visibility="visible"
+              className="border-transparent bg-[#08090c]/82"
+            />
+          ) : null}
+          {topologyRelationGap ? (
+            <HzTopologyActionLink
+              data-topology-relation-gap-action="otlp-trace-call"
+              data-topology-relation-gap-action-owner="hertzbeat-ui-action-link"
+              data-topology-relation-gap-action-source-kind="otlp-trace-call"
+              data-topology-relation-gap-action-view-mode="service-call"
+              data-topology-relation-gap-action-href={topologyRelationGapTraceCallHref}
+              id="relation-gap-trace-call"
+              href={topologyRelationGapTraceCallHref}
+              label={t('topology.relation-gap.action.trace-call')}
+              copy={t('topology.relation-gap.action.trace-call.copy')}
+              emphasis="primary"
+              spacing="none"
+              onClick={handleTopologyRelationGapTraceCall}
+              className="pointer-events-auto absolute left-3 top-[76px] z-10 w-[220px] border-[#252832] bg-[#0b0c0f]/92 px-2.5 py-1.5 shadow-[0_10px_28px_rgba(0,0,0,0.24)] backdrop-blur sm:left-4 sm:top-[80px]"
+            />
+          ) : null}
           <HzTopologyG6Canvas
             graph={topologyG6Graph}
             selectedNodeId={topologyTraceCallMissingEdges ? undefined : primaryNode?.id}
@@ -1573,12 +1946,18 @@ export default function TopologyPage({
             layout={topologyG6Layout}
             height="workbench"
             overlayMode="non-occluding"
-            summaryLabel={`${t('topology.group-panel.node-count', { count: map.nodes.length })} · ${t('topology.group-panel.edge-count', { count: map.edges.length })}`}
+            summaryLabel={topologyG6SummaryLabel}
+            nodeOnlyExplanationLabel={t('topology.node-only.explanation')}
             fitViewLabel={t('topology.view.fit')}
             resetViewLabel={t('topology.toolbar.reset')}
             zoomInLabel={t('topology.view.zoom-in')}
             zoomOutLabel={t('topology.view.zoom-out')}
             selectedFocusLabel={t('topology.view.focus-selected')}
+            edgeDensityDrilldownLabel={t('topology.edge-density.open-table')}
+            edgeDensityDrilldownDetailLabel={t('topology.edge-density.hidden-in-table', {
+              count: Math.max(0, topologyG6Graph.edges.length - topologyG6RenderedEdgeCount)
+            })}
+            edgeDensityDrilldownTargetId="topology-metric-table"
             onNodeSelect={handleTopologyG6NodeSelect}
             onNodeFocus={handleTopologyG6NodeFocus}
             onEdgeSelect={handleTopologyG6EdgeSelect}
@@ -1631,9 +2010,10 @@ export default function TopologyPage({
               relationType={topologyApiScopeRelationType}
               focusEntityId={routeContext?.entityId ?? 'none'}
               depth={map.apiDepth}
-              resultCount={topologyTraceCallMissingEdges ? 0 : map.nodes.length}
+              resultCount={topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing ? 0 : map.nodes.length}
               evidenceSources={topologyEmptyStateEvidenceSources}
               kind={topologyEmptyStateKind}
+              copyVisibility="assistive"
               placement="canvas-center"
               boundary="canvas"
             />
@@ -1657,14 +2037,15 @@ export default function TopologyPage({
         ) : null}
           </HzTopologyCanvas>
         </HzTopologyWorkbenchSlot>
-        <HzTopologyWorkbenchSlot
-          data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"
-          data-topology-companion-rail-visibility="hidden-from-layout"
-          kind="companion"
-          surface="content"
-          className="hidden"
-          aria-hidden="true"
-        >
+        {topologyHasRenderableGraph ? (
+          <HzTopologyWorkbenchSlot
+            data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"
+            data-topology-companion-rail-visibility={topologyCompanionRailVisibility}
+            kind="companion"
+            surface="content"
+            className={topologyShouldShowCompanionRail ? undefined : 'hidden'}
+            aria-hidden={topologyShouldShowCompanionRail ? undefined : true}
+          >
         <HzTopologyCompanionRail
           data-topology-companion-rail-owner="hertzbeat-ui-companion-rail"
           data-topology-companion-rail-boundary-owner="hertzbeat-ui-companion-rail-boundary"
@@ -1681,8 +2062,115 @@ export default function TopologyPage({
             ariaLabel={t('topology.companion.jump.aria')}
             density="graph-first"
             activeMode="contained-rail-scroll"
+            activeResetKey={topologyCompanionSelectionResetKey}
             items={topologyCompanionJumpItems}
           />
+          {topologyDetailEdge ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="edge-detail"
+              data-topology-companion-section-collapsible="edge-detail"
+              sectionId="edge-detail"
+              anchorId="topology-companion-edge-detail"
+              density="graph-first"
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['edge-detail'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-detail', collapsed)}
+            >
+              <HzTopologyDetailDrawer
+                data-topology-edge-evidence-panel={topologyDetailEdge.id}
+                data-topology-edge-evidence-window-owner="hertzbeat-ui-detail-drawer"
+                data-topology-edge-evidence-first-seen={topologyDetailEdge.evidence.firstSeen || 'none'}
+                data-topology-edge-evidence-last-seen={topologyDetailEdge.evidence.lastSeen || 'none'}
+                data-topology-edge-evidence-sample-trace-id={topologyDetailEdge.evidence.sampleTraceId || 'none'}
+                data-topology-edge-evidence-sample-span-id={topologyDetailEdge.evidence.sampleSpanId || 'none'}
+                data-topology-edge-render-window-visibility={topologyDetailEdgeRenderWindowContext?.visibility ?? 'direct'}
+                data-topology-edge-render-window-source-visible={topologyDetailEdgeRenderWindowContext?.sourceVisible ?? 'unknown'}
+                data-topology-edge-render-window-target-visible={topologyDetailEdgeRenderWindowContext?.targetVisible ?? 'unknown'}
+                data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"
+                data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"
+                kind="edge"
+                density="graph-first"
+                subjectId={topologyDetailEdge.id}
+                sourceId={topologyDetailEdge.from}
+                targetId={topologyDetailEdge.to}
+                relationType={topologyDetailEdge.relationshipType}
+                sourceKind={topologyDetailEdge.source}
+                surface="framed"
+                eyebrow={t('topology.edge.evidence.title')}
+                title={topologyDetailEdge.evidence.title}
+                subtitle={`${topologyDetailEdge.evidence.sourceLabel} · ${topologyDetailEdge.evidence.collectedBy}`}
+                boundary={topologyDetailEdge.evidence.boundary}
+                boundaryProps={{
+                  'data-topology-edge-evidence-boundary': 'roadmap-boundary',
+                  'data-topology-detail-boundary-owner': 'hertzbeat-ui-detail-boundary'
+                } as React.HTMLAttributes<HTMLDivElement>}
+                facts={buildTopologyEdgeDetailFacts(topologyDetailEdge, t, topologyDetailEdgeRenderWindowContext)}
+                actions={buildTopologyEdgeDetailActions(topologyDetailEdge, t)}
+                signalActions={buildTopologyEdgeSignalActions(topologyDetailEdge, t)}
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
+          {primaryNode ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="current-node"
+              data-topology-companion-section-collapsible="current-node"
+              sectionId="current-node"
+              anchorId="topology-companion-current-node"
+              density="graph-first"
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['current-node'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('current-node', collapsed)}
+            >
+              <HzTopologyDetailDrawer
+                data-topology-current-entity-panel={primaryNode.id}
+                data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"
+                data-topology-current-entity-panel-surface-owner="hertzbeat-ui-detail-surface"
+                data-topology-current-entity-panel-hover-continuity="selected-node-survives-edge-hover"
+                data-topology-current-entity-panel-hover-invariants="selected-node-detail edge-hover-path-summary-temporary pointer-leave-clears-hover no-url-change no-remount no-refit viewport-preserved render-key-stable"
+                data-topology-current-entity-panel-rail-scroll-continuity="selected-node-retakes-companion-first-viewport"
+                data-topology-current-entity-panel-rail-scroll-invariants="manual-active-resets-scroll-active active-section-scroll collapse-state-preserved no-url-change no-remount no-refit viewport-preserved render-key-stable"
+                data-topology-current-entity-panel-rail-fit="compact-side-rail"
+                data-topology-current-entity-panel-rail-fit-invariants="bounded-block internal-scroll no-page-scroll no-rail-overflow scan-first-viewport"
+                data-topology-current-entity-panel-signal-entry="header-dock"
+                data-topology-current-entity-panel-signal-entry-invariants="metrics-logs-traces-before-long-evidence sticky-with-node-context no-extra-navigation"
+                data-topology-current-entity-panel-scroll-reset="identity-change"
+                data-topology-current-entity-panel-scroll-reset-invariants="identity-change scroll-top-zero no-page-scroll no-rail-scroll no-url-change"
+                data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"
+                data-topology-current-entity-upstream-count={currentNodeNeighborEvidence?.upstreamNodeIds.length ?? 0}
+                data-topology-current-entity-downstream-count={currentNodeNeighborEvidence?.downstreamNodeIds.length ?? 0}
+                data-topology-current-entity-upstream-node-ids={currentNodeNeighborEvidence?.upstreamNodeIds.join(' ') || 'none'}
+                data-topology-current-entity-downstream-node-ids={currentNodeNeighborEvidence?.downstreamNodeIds.join(' ') || 'none'}
+                data-topology-current-entity-group-by={map.filterContext.groupBy}
+                data-topology-current-entity-source-kind={map.filterContext.sourceKind ?? 'all'}
+                data-topology-current-entity-view-mode={map.filterContext.viewMode}
+                kind="node"
+                density="graph-first"
+                subjectId={primaryNode.id}
+                entityType={primaryNode.entityType}
+                sourceKind={primaryNode.source}
+                surface="framed"
+                eyebrow={t('topology.current-entity')}
+                title={primaryNode.label}
+                subtitle={`${primaryNode.entityId} · ${primaryNode.namespace} / ${primaryNode.environment}`}
+                boundary={t('topology.current-entity.boundary')}
+                boundaryProps={{
+                  'data-topology-current-entity-boundary-owner': 'hertzbeat-ui-detail-boundary'
+                } as React.HTMLAttributes<HTMLDivElement>}
+                facts={buildTopologyNodeDetailFacts(
+                  primaryNode,
+                  currentNodeNeighborEvidence ?? buildTopologyNodeNeighborEvidence(primaryNode, map.nodes, map.edges),
+                  t
+                )}
+                actions={buildTopologyNodeDetailActions(primaryNode, map.filterContext, map.apiDepth, t)}
+                signalActions={buildTopologyNodeSignalActions(primaryNode, map.filterContext, map.apiDepth, t)}
+                signalActionsLabel={t('topology.context-link.signals')}
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
           <HzTopologyCompanionSection
             data-topology-companion-section="view-mode"
             sectionId="view-mode"
@@ -1738,38 +2226,41 @@ export default function TopologyPage({
             />
           </HzTopologyCompanionSection>
 
-          <HzTopologyCompanionSection
-            data-topology-companion-section="edge-red"
-            data-topology-companion-section-collapsible="edge-red"
-            sectionId="edge-red"
-            anchorId="topology-companion-edge-red"
-            density="graph-first"
-            collapsible
-            collapsed={topologyCompanionCollapsedSections['edge-red'] ?? false}
-            collapseLabel={t('topology.companion.section.collapse')}
-            expandLabel={t('topology.companion.section.expand')}
-            onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-red', collapsed)}
-          >
-            <HzTopologyMetricTable
-              data-topology-metric-table-owner="hertzbeat-ui"
-              data-topology-metric-table-scope="edge-red-companion"
-              data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
-              data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
-              data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
-              data-topology-metric-table-filter-behavior="in-page-no-route-change"
-              title={t('topology.metric-table.title')}
+          {topologyShouldRenderCompanionMetricTable ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="edge-red"
+              data-topology-companion-section-collapsible="edge-red"
+              sectionId="edge-red"
+              anchorId="topology-companion-edge-red"
               density="graph-first"
-              rows={topologyMetricRows}
-              selectedRowId={topologyMetricSelectedRowId}
-              renderWindowFilter={topologyMetricWindowFilter}
-              onRenderWindowFilterChange={setTopologyMetricWindowFilter}
-              renderWindowCompanion={topologyMetricRenderWindowCompanion}
-              emptyLabel={t('topology.metric-table.empty')}
-              labels={topologyMetricTableLabels}
-              onRowSelect={handleTopologyMetricRowSelect}
-              boundary="framed"
-            />
-          </HzTopologyCompanionSection>
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['edge-red'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-red', collapsed)}
+            >
+              <HzTopologyMetricTable
+                data-topology-metric-table-owner="hertzbeat-ui"
+                data-topology-metric-table-scope="edge-red-companion"
+                data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
+                data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
+                data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
+                data-topology-metric-table-filter-behavior="in-page-no-route-change"
+                title={t('topology.metric-table.title')}
+                density="graph-first"
+                rows={topologyMetricRows}
+                selectedRowId={topologyMetricSelectedRowId}
+                selectionSource={topologyLocalSelection.source}
+                renderWindowFilter={topologyMetricWindowFilter}
+                onRenderWindowFilterChange={setTopologyMetricWindowFilter}
+                renderWindowCompanion={topologyMetricRenderWindowCompanion}
+                emptyLabel={t('topology.metric-table.empty')}
+                labels={topologyMetricTableLabels}
+                onRowSelect={handleTopologyMetricRowSelect}
+                boundary="framed"
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
 
           {map.impactTimeline.length > 0 ? (
             <HzTopologyCompanionSection
@@ -1807,102 +2298,9 @@ export default function TopologyPage({
             </HzTopologyCompanionSection>
           ) : null}
 
-          {topologyDetailEdge ? (
-            <HzTopologyCompanionSection
-              data-topology-companion-section="edge-detail"
-              data-topology-companion-section-collapsible="edge-detail"
-              sectionId="edge-detail"
-              anchorId="topology-companion-edge-detail"
-              density="graph-first"
-              collapsible
-              collapsed={topologyCompanionCollapsedSections['edge-detail'] ?? false}
-              collapseLabel={t('topology.companion.section.collapse')}
-              expandLabel={t('topology.companion.section.expand')}
-              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-detail', collapsed)}
-            >
-              <HzTopologyDetailDrawer
-                data-topology-edge-evidence-panel={topologyDetailEdge.id}
-                data-topology-edge-evidence-window-owner="hertzbeat-ui-detail-drawer"
-                data-topology-edge-evidence-first-seen={topologyDetailEdge.evidence.firstSeen || 'none'}
-                data-topology-edge-evidence-last-seen={topologyDetailEdge.evidence.lastSeen || 'none'}
-                data-topology-edge-evidence-sample-trace-id={topologyDetailEdge.evidence.sampleTraceId || 'none'}
-                data-topology-edge-evidence-sample-span-id={topologyDetailEdge.evidence.sampleSpanId || 'none'}
-                data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"
-                data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"
-                kind="edge"
-                density="graph-first"
-                subjectId={topologyDetailEdge.id}
-                sourceId={topologyDetailEdge.from}
-                targetId={topologyDetailEdge.to}
-                relationType={topologyDetailEdge.relationshipType}
-                sourceKind={topologyDetailEdge.source}
-                surface="framed"
-                eyebrow={t('topology.edge.evidence.title')}
-                title={topologyDetailEdge.evidence.title}
-                subtitle={`${topologyDetailEdge.evidence.sourceLabel} · ${topologyDetailEdge.evidence.collectedBy}`}
-                boundary={topologyDetailEdge.evidence.boundary}
-                boundaryProps={{
-                  'data-topology-edge-evidence-boundary': 'roadmap-boundary',
-                  'data-topology-detail-boundary-owner': 'hertzbeat-ui-detail-boundary'
-                } as React.HTMLAttributes<HTMLDivElement>}
-                facts={buildTopologyEdgeDetailFacts(topologyDetailEdge, t)}
-                actions={buildTopologyEdgeDetailActions(topologyDetailEdge, t)}
-                signalActions={buildTopologyEdgeSignalActions(topologyDetailEdge, t)}
-              />
-            </HzTopologyCompanionSection>
-          ) : null}
-
-          {primaryNode ? (
-            <HzTopologyCompanionSection
-              data-topology-companion-section="current-node"
-              data-topology-companion-section-collapsible="current-node"
-              sectionId="current-node"
-              anchorId="topology-companion-current-node"
-              density="graph-first"
-              collapsible
-              collapsed={topologyCompanionCollapsedSections['current-node'] ?? false}
-              collapseLabel={t('topology.companion.section.collapse')}
-              expandLabel={t('topology.companion.section.expand')}
-              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('current-node', collapsed)}
-            >
-              <HzTopologyDetailDrawer
-                data-topology-current-entity-panel={primaryNode.id}
-                data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"
-                data-topology-current-entity-panel-surface-owner="hertzbeat-ui-detail-surface"
-                data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"
-                data-topology-current-entity-upstream-count={currentNodeNeighborEvidence?.upstreamNodeIds.length ?? 0}
-                data-topology-current-entity-downstream-count={currentNodeNeighborEvidence?.downstreamNodeIds.length ?? 0}
-                data-topology-current-entity-upstream-node-ids={currentNodeNeighborEvidence?.upstreamNodeIds.join(' ') || 'none'}
-                data-topology-current-entity-downstream-node-ids={currentNodeNeighborEvidence?.downstreamNodeIds.join(' ') || 'none'}
-                data-topology-current-entity-group-by={map.filterContext.groupBy}
-                data-topology-current-entity-source-kind={map.filterContext.sourceKind ?? 'all'}
-                data-topology-current-entity-view-mode={map.filterContext.viewMode}
-                kind="node"
-                density="graph-first"
-                subjectId={primaryNode.id}
-                entityType={primaryNode.entityType}
-                sourceKind={primaryNode.source}
-                surface="framed"
-                eyebrow={t('topology.current-entity')}
-                title={primaryNode.label}
-                subtitle={`${primaryNode.entityId} · ${primaryNode.namespace} / ${primaryNode.environment}`}
-                boundary={t('topology.current-entity.boundary')}
-                boundaryProps={{
-                  'data-topology-current-entity-boundary-owner': 'hertzbeat-ui-detail-boundary'
-                } as React.HTMLAttributes<HTMLDivElement>}
-                facts={buildTopologyNodeDetailFacts(
-                  primaryNode,
-                  currentNodeNeighborEvidence ?? buildTopologyNodeNeighborEvidence(primaryNode, map.nodes, map.edges),
-                  t
-                )}
-                actions={buildTopologyNodeDetailActions(primaryNode, map.filterContext, map.apiDepth, t)}
-                signalActions={buildTopologyNodeSignalActions(primaryNode, map.filterContext, map.apiDepth, t)}
-                signalActionsLabel={t('topology.context-link.signals')}
-              />
-            </HzTopologyCompanionSection>
-          ) : null}
         </HzTopologyCompanionRail>
-        </HzTopologyWorkbenchSlot>
+          </HzTopologyWorkbenchSlot>
+        ) : null}
       </HzTopologyWorkbenchGrid>
 
       {topologyShouldShowRenderWindowMetricTable && topologyMetricRows.length > 0 ? (
@@ -1912,6 +2310,7 @@ export default function TopologyPage({
           data-topology-metric-table-placement="graph-bottom"
           data-topology-metric-table-visibility="render-window-companion"
           data-topology-metric-table-scope="edge-red-render-window"
+          data-topology-metric-table-dom-policy="single-interactive-table"
           data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
           data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
           data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
@@ -1920,6 +2319,7 @@ export default function TopologyPage({
           density="graph-first"
           rows={topologyMetricRows}
           selectedRowId={topologyMetricSelectedRowId}
+          selectionSource={topologyLocalSelection.source}
           renderWindowFilter={topologyMetricWindowFilter}
           onRenderWindowFilterChange={setTopologyMetricWindowFilter}
           renderWindowCompanion={topologyMetricRenderWindowCompanion}
@@ -1930,7 +2330,7 @@ export default function TopologyPage({
         />
       ) : null}
 
-      {map.faultContextRows.length > 0 ? (
+      {topologyHasRenderableGraph && map.faultContextRows.length > 0 ? (
         <HzTopologyEvidenceList
           data-topology-fault-context="incoming-evidence"
           data-topology-fault-context-owner="hertzbeat-ui-evidence-list"
@@ -1950,30 +2350,33 @@ export default function TopologyPage({
         />
       ) : null}
 
-      <HzTopologyGroupPanel
-        data-topology-group-panel="large-graph-grouping"
-        data-topology-group-panel-owner="hertzbeat-ui-group-panel"
-        data-topology-group-panel-boundary-owner="hertzbeat-ui-group-panel-boundary"
-        title={t('topology.group-panel.title')}
-        copy={t('topology.group-panel.copy')}
-        groupByLabel={t('topology.group-panel.group-by')}
-        boundary="framed"
-        items={topologyGroupPanelItems}
-        actions={[
-          { id: 'clear-group', href: '/topology', label: t('topology.group-panel.clear-group') },
-          { id: 'open-table', href: '#topology-metric-table', label: t('topology.group-panel.open-table') }
-        ]}
-      />
+      {topologyHasRenderableGraph ? (
+        <HzTopologyGroupPanel
+          data-topology-group-panel="large-graph-grouping"
+          data-topology-group-panel-owner="hertzbeat-ui-group-panel"
+          data-topology-group-panel-boundary-owner="hertzbeat-ui-group-panel-boundary"
+          title={t('topology.group-panel.title')}
+          copy={t('topology.group-panel.copy')}
+          groupByLabel={t('topology.group-panel.group-by')}
+          boundary="framed"
+          items={topologyGroupPanelItems}
+          actions={[
+            { id: 'clear-group', href: '/topology', label: t('topology.group-panel.clear-group') },
+            { id: 'open-table', href: '#topology-metric-table', label: t('topology.group-panel.open-table') }
+          ]}
+        />
+      ) : null}
 
       {topologyInvestigationEdge && topologyHoverFromNode && topologyHoverToNode ? (
         <HzTopologyPathSummary
           data-topology-path-summary="selected-edge-context"
           data-topology-path-summary-owner="hertzbeat-ui-path-summary"
           data-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"
-          title={t('topology.path-summary.title')}
+          title={topologyPathSummaryTitle}
           boundary="section"
-          selectedEdgeId={topologyInvestigationEdge.id}
-          hoveredEdgeId={topologyInvestigationEdge.id}
+          interactionState={topologyPathSummaryInteractionState}
+          selectedEdgeId={topologyCanvasSelectedEdgeId}
+          hoveredEdgeId={topologyG6HoveredEdgeId}
           sourceId={topologyHoverFromNode.id}
           targetId={topologyHoverToNode.id}
           relationType={topologyInvestigationEdge.relationshipType}
diff --git a/web-next/app/topology/topology-route-client.tsx b/web-next/app/topology/topology-route-client.tsx
new file mode 100644
index 0000000000..3ce7aa581a
--- /dev/null
+++ b/web-next/app/topology/topology-route-client.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import React from 'react';
+import dynamic from 'next/dynamic';
+import { preloadHzTopologyG6Runtime } from '@hertzbeat/ui/topology-g6-runtime';
+import TopologyRouteShell from './topology-route-shell';
+import { preloadTopologyGraph, resolveTopologyApiTimeoutMs } from '../../lib/topology-surface/controller';
+import type { TopologyRouteContext } from '../../lib/topology-surface/view-model';
+
+void preloadHzTopologyG6Runtime();
+
+const TopologyPage = dynamic(() => import('./topology-page'), {
+  ssr: false,
+  loading: () => <TopologyRouteShell />
+});
+
+export default function TopologyRouteClient({
+  routeContext,
+  shellElementId
+}: {
+  routeContext?: TopologyRouteContext;
+  shellElementId?: string;
+}) {
+  const routeContextKey = React.useMemo(() => JSON.stringify(routeContext ?? {}), [routeContext]);
+  const topologyScaleProof = routeContext?.scaleProof?.trim();
+
+  React.useEffect(() => {
+    if (routeContext?.scaleProof?.trim()) return;
+    void preloadTopologyGraph(routeContext ?? {}, { timeoutMs: resolveTopologyApiTimeoutMs(routeContext) });
+  }, [routeContext, routeContextKey]);
+
+  React.useEffect(() => {
+    if (!shellElementId) return;
+    document.getElementById(shellElementId)?.setAttribute('hidden', '');
+  }, [shellElementId]);
+
+  return (
+    <>
+      <span hidden data-topology-route-client-prefetch="topology-api-before-heavy-page" />
+      {topologyScaleProof ? <span hidden data-topology-route-client-scale-proof-prefetch="skipped" /> : null}
+      <TopologyPage routeContext={routeContext} />
+    </>
+  );
+}
diff --git a/web-next/app/topology/topology-route-shell.tsx b/web-next/app/topology/topology-route-shell.tsx
new file mode 100644
index 0000000000..d7690da3cd
--- /dev/null
+++ b/web-next/app/topology/topology-route-shell.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+export default function TopologyRouteShell() {
+  return (
+    <main
+      className="min-h-[calc(100vh-56px)] bg-[#08090c] px-4 py-6 text-[#f1f3f7]"
+      data-topology-route-shell="deferred-client-entry"
+      data-topology-route-shell-owner="next-dynamic-client-entry"
+    >
+      <section className="mx-auto flex max-w-[1440px] flex-col gap-3">
+        <div className="flex items-center justify-between gap-3">
+          <div>
+            <div className="h-3 w-14 rounded-[2px] bg-[#1b2230]" />
+            <div className="mt-3 h-6 w-28 rounded-[3px] bg-[#202838]" />
+          </div>
+          <div className="flex gap-2">
+            <div className="h-8 w-14 rounded-[3px] bg-[#121722]" />
+            <div className="h-8 w-20 rounded-[3px] bg-[#121722]" />
+          </div>
+        </div>
+        <div className="h-10 rounded-[3px] border border-[#1e2634] bg-[#0c1119]" />
+        <div className="h-[640px] rounded-[3px] border border-[#1e2634] bg-[#07090d]" />
+      </section>
+    </main>
+  );
+}
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index fe58142a55..876a2a4d2d 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2167,8 +2167,12 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
     expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
     expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
+    expect(html).toContain('data-hz-topology-toolbar-control-flow="single-grid-row"');
+    expect(html).toContain('data-hz-topology-toolbar-empty-offset="none"');
+    expect(html).toContain('[grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-display="contents"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
     expect(html).toContain('data-hz-topology-toolbar-secondary-visibility="assistive"');
     expect(html).toContain('data-hz-topology-toolbar-boundary="none"');
@@ -2282,6 +2286,10 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
     expect(html).toContain('data-hz-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"');
     expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="checkout-orders"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-selected-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-hovered-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-source-id="svc-checkout"');
@@ -2372,6 +2380,13 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-node-icon-source="lucide-entity-type-catalog"');
     expect(html).toContain('data-hz-topology-g6-node-icon-library="lucide-react"');
     expect(html).toContain('data-hz-topology-g6-node-icon-no-handdrawn="true"');
+    expect(html).toContain('data-hz-topology-node-icon-kind="endpoint"');
+    expect(html).toContain('data-hz-topology-node-icon-name="route"');
+    expect(html).toContain('data-hz-topology-g6-no-handdrawn-icons="true"');
+    expect(html).toContain('data-hz-topology-g6-icon-source-proof="lucide-react:entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-node-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-node-icon-source="entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-node-icon-no-handdrawn="true"');
     expect(html).toContain('data-hz-topology-g6-engine="antv-g6"');
     expect(html).toContain('data-hz-topology-g6-interaction-owner="hertzbeat-ui-g6-interaction"');
     expect(html).toContain('data-hz-topology-g6-auto-fit="initial-only"');
@@ -2385,6 +2400,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-command-event="hz-topology-g6-viewport-command"');
     expect(html).toContain('data-hz-topology-g6-command-request="idle"');
     expect(html).toContain('data-hz-topology-g6-command-request-owner="hertzbeat-ui-g6-command-request"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard="drag-pan-suppresses-click-selection"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard-owner="hertzbeat-ui-g6-pan-selection-guard"');
     expect(html).toContain('data-hz-topology-g6-wheel-mode="manual-clamped-g6-zoom"');
     expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
     expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
@@ -2397,8 +2414,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-minimap="viewport-overview"');
     expect(html).toContain('data-hz-topology-g6-minimap-owner="hertzbeat-ui-g6-minimap"');
     expect(html).toContain('data-hz-topology-g6-minimap-visibility="visible"');
-    expect(html).toContain('data-hz-topology-g6-minimap-node-count="7"');
-    expect(html).toContain('data-hz-topology-g6-minimap-edge-count="6"');
+    expect(html).toContain('data-hz-topology-g6-minimap-node-count="8"');
+    expect(html).toContain('data-hz-topology-g6-minimap-edge-count="7"');
     expect(html).toContain('data-hz-topology-g6-minimap-viewport-source="initial"');
     expect(html).toContain('data-hz-topology-g6-node-selection="drawer-sync"');
     expect(html).toContain('data-hz-topology-g6-edge-selection="drawer-sync"');
@@ -2429,8 +2446,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-neighbor-role="upstream"');
     expect(html).toContain('data-hz-topology-g6-neighbor-role="downstream"');
     expect(html).toContain('data-hz-topology-g6-neighbor-role="outgoing"');
-    expect(html).toContain('data-hz-topology-g6-node-count="7"');
-    expect(html).toContain('data-hz-topology-g6-edge-count="6"');
+    expect(html).toContain('data-hz-topology-g6-node-count="8"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="7"');
     expect(html).toContain('data-hz-topology-g6-action="fit-view"');
     expect(html).toContain('data-hz-topology-g6-filter-owner="hertzbeat-ui-g6-filter"');
     expect(html).toContain('data-hz-topology-g6-filter-environment="prod"');
@@ -2459,6 +2476,9 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-group-surface-visibility="assistive"');
     expect(html).toContain('data-hz-topology-g6-filter-control-surface-visibility="assistive"');
     expect(html).toContain('data-hz-topology-g6-toolbar-visibility="visible"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-node-only-relation-state="shared"');
+    expect(html).toContain('data-hz-topology-g6-relation-state="entities-without-relation-edges"');
+    expect(html).toContain('data-hz-topology-g6-node-only-explanation-owner="hertzbeat-ui-g6-node-only-explanation"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-scale-contract="50-200-500"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-scale-canvas="500-node-windowed-shared"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-scale-50="50"');
@@ -2468,6 +2488,11 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-strategy="grouped-table-companion"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-filtering="required"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-table-companion="required"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-overflow="650"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-overflow-strategy="filter-first"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-overflow-policy="filter-first-before-expanded-render"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-overflow-rendered="200"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-overflow-hidden="450"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-render-window="500-node-windowed-shared"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-mode="windowed"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-rendered="200"');
@@ -2477,12 +2502,72 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-g6-render-window-visible-node-budget="200"');
     expect(html).toContain('data-hz-topology-g6-render-window-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-g6-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-render-window-candidate-edge-count=');
+    expect(html).toContain(
+      'data-hz-topology-g6-render-window-rendered-edge-count-owner="hertzbeat-ui-g6-edge-density"'
+    );
     expect(html).toContain('data-hz-topology-g6-render-window-hidden-node-count="300"');
     expect(html).toContain('data-hz-topology-g6-render-window-table-companion="required"');
+    expect(html).toContain('data-hz-topology-g6-summary-count-policy="windowed-rendered-vs-total"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-g6-summary-total-node-count="500"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-edge-count="180"');
+    expect(html).toContain('200/500 nodes rendered · 180/');
+    expect(html).not.toContain('>500 nodes ·');
+    expect(html).toContain('data-hz-topology-g6-edge-label-owner="hertzbeat-ui-g6-edge-label"');
+    expect(html).toContain('data-hz-topology-g6-edge-label-policy="hidden-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-edge-label-visible-count="0"');
+    expect(html).toContain('data-hz-topology-g6-node-label-owner="hertzbeat-ui-g6-node-label"');
+    expect(html).toContain('data-hz-topology-g6-node-label-policy="hub-only-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-node-label-metadata-policy="preserve-data-label"');
+    expect(html).toContain('data-hz-topology-g6-shape-profile-owner="hertzbeat-ui-g6-shape-profile"');
+    expect(html).toContain('data-hz-topology-g6-shape-profile-evidence="degree-derived"');
+    expect(html).toContain('data-hz-topology-g6-shape-profile=');
+    expect(html).toContain('data-hz-topology-g6-edge-density-owner="hertzbeat-ui-g6-edge-density"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-policy="reduced-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-max-visible="180"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-owner="hertzbeat-ui-g6-edge-readability"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-policy="attenuated-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-ranking-policy="red-priority-stable-render-order"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-stability="selection-hover-invariant"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-max-prominent-edge-count="72"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-minimum-opacity="0.2"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-owner="hertzbeat-ui-g6-edge-density-affordance"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-action="scroll-edge-table"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-target="topology-metric-table"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-live-owner="hertzbeat-ui-g6-edge-density-affordance"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-invariants="table-scroll no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-owner="hertzbeat-ui-g6-semantic-cluster"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-policy="hub-fanout-summary"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-item-count="4"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-item-role="hub"');
+    expect(html).toContain('data-hz-topology-g6-windowed-layout-owner="hertzbeat-ui-g6-windowed-layout"');
+    expect(html).toContain('data-hz-topology-g6-windowed-layout-policy="packed-lanes"');
+    expect(html).toContain('data-hz-topology-g6-windowed-layout-max-lane-rows="18"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-owner="hertzbeat-ui-g6-scale-performance"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-policy="windowed-interactive-budget"');
+    expect(html).toContain(
+      'data-hz-topology-g6-scale-performance-invariants="windowed-render pan-zoom-no-url-change no-remount no-refit viewport-preserved render-key-stable table-companion"'
+    );
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-node-budget="200"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-edge-budget="180"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-owner="hertzbeat-ui-g6-mount-lifecycle"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-policy="structure-layout-height-only"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-redraw-policy="setData-draw-preserve-viewport"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key=');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-owner="hertzbeat-ui-g6-structure-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-length=');
+    expect(html).not.toContain('scale-svc-000:service|');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-owner="hertzbeat-ui-g6-render-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-length=');
+    expect(html).not.toContain('scale-svc-000::#');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state="pending"');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state-owner="hertzbeat-ui-g6-fit-settle-state"');
     expect(html).toContain('data-hz-topology-g6-render-window-priority-node-count="1"');
     expect(html).toContain('data-hz-topology-g6-render-window-priority-node-ids="scale-svc-420"');
     expect(source).toContain('const [topologyG6ScaleLabSelection, setTopologyG6ScaleLabSelection]');
@@ -2503,7 +2588,20 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain(
       'data-hz-ui-lab-topology-g6-scale-browser-regression-invariants="no-url-change no-remount viewport-preserved table-companion"'
     );
+    expect(html).toContain('data-hz-topology-g6-live-interaction-owner="hertzbeat-ui-g6-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-live-interaction-invariants="no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-owner="hertzbeat-ui-g6-edge-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-invariants="edge-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-selected-node-rendered=');
+    expect(html).toContain('data-hz-topology-g6-selected-edge-rendered=');
+    expect(html).toContain('data-hz-ui-lab-static-confirm-preview="non-blocking-inline"');
+    expect(source).toContain('uiLabStaticConfirmDialogPreviewClassName');
+    expect(source).toContain('data-hz-ui-lab-static-confirm-preview="non-blocking-inline"');
+    expect(source).not.toContain('<HzConfirmDialog\n                open\n                tone="critical"');
+    expect(source).not.toContain('<HzConfirmDialog\n                    open\n                    tone="critical"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
+    expect(html).toContain('data-hz-topology-metric-table-root="true"');
+    expect(html).toContain('data-hz-topology-metric-table-total-rows="3"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="500"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-node-count="200"');
@@ -2513,6 +2611,10 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-metric-table-visible-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-partial-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-row-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-owner="hertzbeat-ui-metric-table-hidden-row-proof"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof="available"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-filter="hidden"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-unknown-row-count="0"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
@@ -2530,6 +2632,9 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-search-query="Service 420"');
     expect(html).toContain('data-hz-topology-g6-search-match-count="1"');
     expect(html).toContain('data-hz-topology-g6-search-first-match="scale-svc-420"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-action-count="4"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-visible-actions="zoom-out zoom-in fit-view reset-view"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-focus-actions-visibility="assistive"');
     expect(html).toContain('data-hz-topology-g6-action="focus-search-result"');
     expect(html).toContain('data-hz-topology-g6-selected-focus-owner="hertzbeat-ui-g6-selected-focus"');
     expect(html).toContain('data-hz-topology-g6-selected-focus-target="scale-svc-420"');
@@ -2633,6 +2738,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-legend-dock="shared"');
     expect(html).toContain('data-hz-topology-g6-legend-dock="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-state="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-container="true"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-placement="canvas-bottom-left"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-occlusion="inside-canvas-low-interruption"');
@@ -2696,41 +2803,41 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-legend-boundary="flush"');
     expect(html).toContain('data-hz-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"');
 	    expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
-	    expect(html).toContain('data-hz-topology-legend-section="status"');
-	    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
-	    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
-	    expect(html).toContain('data-hz-topology-legend-section="node-type"');
-	    expect(html).toContain('data-hz-topology-legend-section="interaction"');
-	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-cache"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-queue"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-monitor"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-middleware"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-application"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
-	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="memory-stick"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="inbox"');
-	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
-	    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
-	    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
-	    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
-	    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+    expect(html).not.toContain('data-hz-topology-legend-section="status"');
+    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).not.toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-cache"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-queue"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-monitor"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-middleware"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-application"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
+    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="route"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="memory-stick"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="inbox"');
+    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
     expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('data-hz-ui-lab-topology-detail-drawer="shared"');
     expect(html).toContain('data-hz-ui="topology-detail-drawer"');
@@ -2747,6 +2854,16 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="edge:api-warehouse:svc-api:db-warehouse:trace-call:otlp-trace-call:unknown"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement="header-dock"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement-owner="hertzbeat-ui-detail-signal-action-placement"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-sticky="top-with-header-context"');
     expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
     expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
     expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
@@ -2782,6 +2899,13 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="node:svc-checkout:none:none:unknown:otlp-trace-call:service"');
     expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
     expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
     expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
@@ -2834,9 +2958,20 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-metric-table-header-owner="hertzbeat-ui-metric-table-header"');
     expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
     expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"');
+    expect(html).toContain('data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"');
+    expect(html).toContain(
+      'data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"'
+    );
     expect(html).toContain('data-hz-ui-lab-topology-metric-table-scale-selection="edge-sync"');
     expect(html).toContain('data-hz-ui-lab-topology-metric-table-scale-selection-owner="hertzbeat-ui-metric-table-scale-selection"');
     expect(html).toContain('data-hz-topology-edge-row="scale-edge-420-421"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-mode="table-row-click-drawer"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-edge-selected="true"');
     expect(html).toContain('data-hz-topology-metric-table-route-owner="hertzbeat-ui-metric-table-route"');
     expect(html).toContain('data-hz-topology-metric-table-badge-owner="hertzbeat-ui-metric-table-badge"');
@@ -2852,7 +2987,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-ui-lab-topology-empty-state="shared"');
     expect(html).toContain('data-hz-ui="topology-empty-state"');
     expect(html).toContain('data-hz-topology-primitive="empty-state"');
-    expect(html).toContain('data-hz-topology-empty-boundary="flush"');
+    expect(html).toContain('data-hz-topology-empty-boundary="canvas"');
+    expect(html).toContain('data-hz-topology-empty-boundary-visual="frameless-canvas"');
     expect(html).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
     expect(html).toContain('data-hz-topology-empty-scope-owner="hertzbeat-ui-empty-scope"');
     expect(html).toContain('data-hz-topology-empty-environment="prod"');
@@ -2862,6 +2998,7 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-empty-depth="2"');
     expect(html).toContain('data-hz-topology-empty-result-count="0"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="trace relation monitor incident"');
+    expect(html).toContain('data-hz-topology-empty-copy-visibility="assistive"');
     expect(html).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
     expect(html).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
     expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
@@ -2872,6 +3009,7 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-empty-kind="degraded"');
     expect(html).toContain('data-hz-topology-empty-source="Topology API unavailable"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="api greptime trace relation"');
+    expect(html).toContain('data-hz-topology-empty-copy-visibility="assistive"');
     expect(html).toContain('Topology API unavailable');
     expect(html).toContain('data-hz-ui-lab-topology-loading-state="shared"');
     expect(html).toContain('data-hz-ui="topology-loading-state"');
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index 4bf30d9a9c..fdea1c2085 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -204,6 +204,7 @@ import {
 } from '@hertzbeat/ui';
 import {
   HzTopologyG6Canvas,
+  HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT,
   HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
@@ -358,6 +359,17 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
       evidenceBadges: ['trace'],
       redMetrics: { requestRatePerSecond: 7.1, errorRate: 0.012, latencyP95Ms: 92 }
     },
+    {
+      id: 'endpoint-checkout',
+      label: '/api/checkout',
+      entityType: 'endpoint',
+      health: 'healthy',
+      tone: 'blue',
+      focus: 'normal',
+      source: 'otlp-trace-call',
+      evidenceBadges: ['endpoint'],
+      redMetrics: { requestRatePerSecond: 10.4, errorRate: 0.006, latencyP95Ms: 38 }
+    },
     {
       id: 'queue-events',
       label: 'Events Queue',
@@ -404,6 +416,18 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
     }
   ],
   edges: [
+    {
+      id: 'web-endpoint',
+      from: 'svc-web',
+      to: 'endpoint-checkout',
+      label: 'HTTP route',
+      relationshipType: 'trace-call',
+      source: 'otlp-trace-call',
+      tone: 'blue',
+      focus: 'context-muted',
+      evidenceBadges: ['trace'],
+      redMetrics: { requestRatePerSecond: 10.4, errorRate: 0.006, latencyP95Ms: 38 }
+    },
     {
       id: 'web-checkout',
       from: 'svc-web',
@@ -478,6 +502,39 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
   ]
 };
 
+const topologyG6NodeOnlyLabGraph: HzTopologyG6GraphInput = {
+  nodes: [
+    {
+      id: 'node-only-payment',
+      label: 'Payment API',
+      entityType: 'service',
+      health: 'warning',
+      tone: 'warning',
+      source: 'cmdb-manual-label',
+      evidenceBadges: ['cmdb']
+    },
+    {
+      id: 'node-only-checkout',
+      label: 'Checkout API',
+      entityType: 'service',
+      health: 'healthy',
+      tone: 'success',
+      source: 'cmdb-manual-label',
+      evidenceBadges: ['cmdb']
+    },
+    {
+      id: 'node-only-orders',
+      label: 'Orders DB',
+      entityType: 'database',
+      health: 'healthy',
+      tone: 'blue',
+      source: 'cmdb-manual-label',
+      evidenceBadges: ['cmdb']
+    }
+  ],
+  edges: []
+};
+
 const topologyG6ScaleLabGraph = buildHzTopologyG6ScaleFixture(50, {
   environment: 'prod',
   namespace: 'commerce',
@@ -488,14 +545,30 @@ const topologyG6ScaleLabGraph500 = buildHzTopologyG6ScaleFixture(500, {
   namespace: 'commerce',
   prefix: 'scale'
 });
+const topologyG6ScaleLabGraphOverflow = buildHzTopologyG6ScaleFixture(650, {
+  environment: 'prod',
+  namespace: 'commerce',
+  prefix: 'scale'
+});
 const topologyG6ScaleLabProfile50 = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraph);
 const topologyG6ScaleLabProfile200 = buildHzTopologyG6ScaleProfile(buildHzTopologyG6ScaleFixture(200));
 const topologyG6ScaleLabProfile500 = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraph500);
+const topologyG6ScaleLabProfileOverflow = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraphOverflow);
 const topologyG6LargeGraphStrategy200 = buildHzTopologyG6LargeGraphStrategy(buildHzTopologyG6ScaleFixture(200));
 const topologyG6LargeGraphStrategy500 = buildHzTopologyG6LargeGraphStrategy(topologyG6ScaleLabGraph500);
+const topologyG6LargeGraphStrategyOverflow = buildHzTopologyG6LargeGraphStrategy(topologyG6ScaleLabGraphOverflow);
 const topologyG6ScaleLabRenderWindow500 = buildHzTopologyG6RenderWindow(topologyG6ScaleLabGraph500, topologyG6LargeGraphStrategy500, {
   priorityNodeIds: ['scale-svc-420']
 });
+const topologyG6ScaleLabRenderedEdgeCount500 = Math.min(
+  topologyG6ScaleLabRenderWindow500.renderedEdgeCount,
+  HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT
+);
+const topologyG6ScaleLabSummary500 = `${topologyG6ScaleLabRenderWindow500.renderedNodeCount}/${topologyG6ScaleLabRenderWindow500.totalNodeCount} nodes rendered · ${topologyG6ScaleLabRenderedEdgeCount500}/${topologyG6ScaleLabRenderWindow500.totalEdgeCount} edges rendered`;
+const topologyG6ScaleLabRenderWindowOverflow = buildHzTopologyG6RenderWindow(
+  topologyG6ScaleLabGraphOverflow,
+  topologyG6LargeGraphStrategyOverflow
+);
 const topologyG6LabVisibleNodeKinds = new Set(
   topologyG6LabGraph.nodes.map(node => getHzTopologyG6NodeIcon(node.entityType).kind).filter(kind => kind !== 'unknown')
 );
@@ -1520,6 +1593,9 @@ function findTemplate(categories: HzTemplateCategory[], id: string) {
   return categories.flatMap(category => category.items).find(item => item.id === id) || categories[0]?.items[0];
 }
 
+const uiLabStaticConfirmDialogPreviewClassName =
+  'pointer-events-none relative inset-auto z-0 min-w-[360px] bg-transparent p-0';
+
 export default function HertzBeatUiLabPage() {
   const { t } = useI18n();
   const [activeTab, setActiveTab] = React.useState('monitors');
@@ -2189,6 +2265,7 @@ export default function HertzBeatUiLabPage() {
               </HzActionGroup>
               <HzConfirmDialog
                 open
+                className={uiLabStaticConfirmDialogPreviewClassName}
                 tone="critical"
                 kicker="Monitor center"
                 title="Please confirm whether to cancel monitor!"
@@ -2206,6 +2283,7 @@ export default function HertzBeatUiLabPage() {
                 data-monitor-row-response-confirm-target="checkout-api"
                 data-monitor-row-response-confirm-closable="angular-nz-closable-false"
                 data-monitor-row-response-confirm-ok="angular-nz-ok-danger-primary"
+                data-hz-ui-lab-static-confirm-preview="non-blocking-inline"
               >
                 <div data-monitor-row-response-confirm-body="angular-single-row-confirm">
                   <HzInlineFeedback
@@ -7014,7 +7092,7 @@ export default function HertzBeatUiLabPage() {
                 <HzTopologyLegend
                   data-hz-ui-lab-topology-g6-legend-dock="shared"
                   title="Legend"
-                  summaryLabel="In canvas"
+                  summaryLabel="Node types"
                   boundary="flush"
                   density="canvas-dock"
                   sections={[
@@ -7022,24 +7100,6 @@ export default function HertzBeatUiLabPage() {
                           id: 'node-type',
                           label: 'Node type',
                           items: topologyG6NodeTypeLegendItems
-                        },
-                        {
-                          id: 'status',
-                          label: 'Status',
-                          items: [
-                            { id: 'healthy-node', label: 'Healthy', color: '#22c55e', visualSource: 'hertzbeat-status-token', value: 'healthy' },
-                            { id: 'warning-node', label: 'Warning', color: '#f59e0b', visualSource: 'hertzbeat-status-token', value: 'warning' },
-                            { id: 'critical-node', label: 'Critical', color: '#ef4444', visualSource: 'hertzbeat-status-token', value: 'critical' }
-                          ]
-                        },
-                        {
-                          id: 'interaction',
-                          label: 'Interaction',
-                          items: [
-                            { id: 'selected-node', label: 'Selected', color: '#e5edf8', visualSource: 'hertzbeat-interaction-token', value: 'selected' },
-                            { id: 'directional-edge', label: 'Edge', color: '#94a3b8', pattern: 'solid', visualSource: 'hertzbeat-edge-token', value: 'direction' },
-                            { id: 'dimmed-edge', label: 'Dimmed', color: '#94a3b8', pattern: 'muted', visualSource: 'hertzbeat-edge-token', value: 'context' }
-                          ]
                         }
                       ]}
                     />
@@ -7059,15 +7119,49 @@ export default function HertzBeatUiLabPage() {
                   data-hz-ui-lab-topology-g6-non-occluding="shared"
                 />
               </HzTopologyCanvas>
+              <HzTopologyCanvas
+                data-hz-ui-lab-topology-g6-node-only-relation-state="shared"
+                layout="layered-service"
+                interactionMode="inspect"
+                interactionScope="hover-group"
+                hoverMode="neighbor-highlight"
+                drawerMode="node-edge"
+                focusDepth="2-hop"
+                minHeight="compact"
+                boundary="section"
+              >
+                <HzTopologyCanvasAnnotation
+                  title="Node-only relation state"
+                  copy="Entities are visible while the current filter has no relation edges."
+                  visibility="assistive"
+                />
+                <HzTopologyG6Canvas
+                  graph={topologyG6NodeOnlyLabGraph}
+                  layout="layered-service"
+                  height="compact"
+                  summaryLabel="3 nodes · 0 edges"
+                  fitViewLabel="Fit view"
+                  resetViewLabel="Reset view"
+                  zoomInLabel="Zoom in"
+                  zoomOutLabel="Zoom out"
+                />
+              </HzTopologyCanvas>
               <HzTopologyCanvas
                 data-hz-ui-lab-topology-g6-scale-contract="50-200-500"
                 data-hz-ui-lab-topology-g6-scale-50={topologyG6ScaleLabProfile50.nodeCount}
                 data-hz-ui-lab-topology-g6-scale-200={topologyG6ScaleLabProfile200.nodeCount}
                 data-hz-ui-lab-topology-g6-scale-500={topologyG6ScaleLabProfile500.nodeCount}
+                data-hz-ui-lab-topology-g6-scale-overflow={topologyG6ScaleLabProfileOverflow.nodeCount}
                 data-hz-ui-lab-topology-g6-large-graph-200-strategy={topologyG6LargeGraphStrategy200.strategy}
                 data-hz-ui-lab-topology-g6-large-graph-500-strategy={topologyG6LargeGraphStrategy500.strategy}
                 data-hz-ui-lab-topology-g6-large-graph-500-filtering={topologyG6LargeGraphStrategy500.filtering}
                 data-hz-ui-lab-topology-g6-large-graph-500-table-companion={topologyG6LargeGraphStrategy500.tableCompanion}
+                data-hz-ui-lab-topology-g6-large-graph-overflow-strategy={topologyG6LargeGraphStrategyOverflow.strategy}
+                data-hz-ui-lab-topology-g6-large-graph-overflow-policy="filter-first-before-expanded-render"
+                data-hz-ui-lab-topology-g6-render-window-overflow-rendered={
+                  topologyG6ScaleLabRenderWindowOverflow.renderedNodeCount
+                }
+                data-hz-ui-lab-topology-g6-render-window-overflow-hidden={topologyG6ScaleLabRenderWindowOverflow.hiddenNodeCount}
                 data-hz-ui-lab-topology-g6-render-window="500-node-windowed-shared"
                 data-hz-ui-lab-topology-g6-render-window-mode={topologyG6ScaleLabRenderWindow500.mode}
                 data-hz-ui-lab-topology-g6-render-window-rendered={topologyG6ScaleLabRenderWindow500.renderedNodeCount}
@@ -7134,7 +7228,7 @@ export default function HertzBeatUiLabPage() {
                       href: '/topology?environment=prod'
                     }
                   ]}
-                  summaryLabel={`${topologyG6ScaleLabProfile500.nodeCount} nodes · ${topologyG6ScaleLabProfile500.edgeCount} edges`}
+                  summaryLabel={topologyG6ScaleLabSummary500}
                   fitViewLabel="Fit view"
                   resetViewLabel="Reset view"
                   zoomInLabel="Zoom in"
@@ -7285,31 +7379,13 @@ export default function HertzBeatUiLabPage() {
                   <HzTopologyLegend
                     data-hz-ui-lab-topology-legend="shared"
                     title="Topology legend"
-                    summaryLabel="3 groups"
+                    summaryLabel="Node types"
                     boundary="flush"
                     sections={[
                       {
                         id: 'node-type',
                         label: 'Node type',
                         items: topologyG6NodeTypeLegendItems
-                      },
-                      {
-                        id: 'status',
-                        label: 'Status',
-                        items: [
-                          { id: 'healthy-node', label: 'Healthy', color: '#22c55e', visualSource: 'hertzbeat-status-token', value: 'healthy' },
-                          { id: 'warning-node', label: 'Warning', color: '#f59e0b', visualSource: 'hertzbeat-status-token', value: 'warning' },
-                          { id: 'critical-node', label: 'Critical', color: '#ef4444', visualSource: 'hertzbeat-status-token', value: 'critical' }
-                        ]
-                      },
-                      {
-                        id: 'interaction',
-                        label: 'Interaction',
-                        items: [
-                          { id: 'selected-node', label: 'Selected node', color: '#e5edf8', visualSource: 'hertzbeat-interaction-token', value: 'selected' },
-                          { id: 'directional-edge', label: 'Directional edge', color: '#94a3b8', pattern: 'solid', visualSource: 'hertzbeat-edge-token', value: 'arrow direction' },
-                          { id: 'dimmed-edge', label: 'Dimmed edge', color: '#94a3b8', pattern: 'muted', visualSource: 'hertzbeat-edge-token', value: 'outside selection' }
-                        ]
                       }
                     ]}
                   />
@@ -7475,7 +7551,8 @@ export default function HertzBeatUiLabPage() {
                   depth={2}
                   resultCount={0}
                   evidenceSources={['trace', 'relation', 'monitor', 'incident']}
-                  boundary="flush"
+                  copyVisibility="assistive"
+                  boundary="canvas"
                 />
                 <HzTopologyEmptyState
                   data-hz-ui-lab-topology-degraded-state="shared"
@@ -7491,6 +7568,7 @@ export default function HertzBeatUiLabPage() {
                   resultCount={0}
                   evidenceSources={['api', 'greptime', 'trace', 'relation']}
                   kind="degraded"
+                  copyVisibility="assistive"
                   boundary="flush"
                 />
                 <HzTopologyLoadingState
@@ -10866,6 +10944,7 @@ export default function HertzBeatUiLabPage() {
                   </HzConfirmDialog>
                   <HzConfirmDialog
                     open
+                    className={uiLabStaticConfirmDialogPreviewClassName}
                     tone="critical"
                     kicker="Monitor center"
                     title="Please confirm whether to cancel monitor in batches!"
@@ -10883,6 +10962,7 @@ export default function HertzBeatUiLabPage() {
                     data-monitor-batch-response-confirm-count="2"
                     data-monitor-batch-response-confirm-closable="angular-nz-closable-false"
                     data-monitor-batch-response-confirm-ok="angular-nz-ok-danger-primary"
+                    data-hz-ui-lab-static-confirm-preview="non-blocking-inline"
                   >
                     <div data-monitor-batch-response-confirm-body="angular-batch-confirm">
                       <HzInlineFeedback
diff --git a/web-next/components/pages/entity-detail-surface.test.tsx b/web-next/components/pages/entity-detail-surface.test.tsx
index 5aa19085de..df16bf361a 100644
--- a/web-next/components/pages/entity-detail-surface.test.tsx
+++ b/web-next/components/pages/entity-detail-surface.test.tsx
@@ -257,6 +257,41 @@ describe('EntityDetailSurface', () => {
     expect(html).not.toContain('data-drawer-section');
   }, 30000);
 
+  it('uses shared signal evidence when deciding whether the entity has investigation context', () => {
+    const html = renderToStaticMarkup(
+      <EntityDetailSurface
+        detail={{
+          entity: {
+            entity: {
+              id: 77,
+              name: 'payment-api',
+              displayName: 'Payment API',
+              type: 'service',
+              status: 'healthy'
+            }
+          },
+          evidenceSummary: {
+            downMonitorCount: 0
+          },
+          alertSummary: {
+            totalActiveAlerts: 0
+          },
+          signalEvidence: {
+            logSummary: { hintCount: 2 },
+            traceSummary: { recentTraceCount: 3 }
+          }
+        } as any}
+        actionError={null}
+        isPending={false}
+        onDelete={() => undefined}
+        onRefresh={() => undefined}
+      />
+    );
+
+    expect(html).toContain('Logs, traces, or monitor evidence are linked and ready for investigation.');
+    expect(html).not.toContain('Add owner, definition, and telemetry binding so this entity becomes usable.');
+  });
+
   it('renders entity context handoff links with inherited time and monitor context', () => {
     const html = renderToStaticMarkup(
       <EntityDetailSurface
diff --git a/web-next/components/pages/entity-detail-surface.tsx b/web-next/components/pages/entity-detail-surface.tsx
index 39211566d5..83e0211dc3 100644
--- a/web-next/components/pages/entity-detail-surface.tsx
+++ b/web-next/components/pages/entity-detail-surface.tsx
@@ -93,7 +93,9 @@ function resolveTitle(detail: EntityDetailDto) {
 function resolveSubtitle(detail: EntityDetailDto) {
   const activeAlerts = detail.alertSummary?.totalActiveAlerts ?? detail.activeAlerts?.length ?? 0;
   const downMonitors = detail.evidenceSummary?.downMonitorCount ?? 0;
-  const hasEvidence = (detail.logSummary?.hintCount ?? 0) > 0 || (detail.traceSummary?.recentTraceCount ?? 0) > 0 || (detail.boundMonitors?.length ?? 0) > 0;
+  const logSummary = detail.signalEvidence?.logSummary || detail.logSummary;
+  const traceSummary = detail.signalEvidence?.traceSummary || detail.traceSummary;
+  const hasEvidence = (logSummary?.hintCount ?? 0) > 0 || (traceSummary?.recentTraceCount ?? 0) > 0 || (detail.boundMonitors?.length ?? 0) > 0;
 
   if (activeAlerts > 0) {
     return translateEntityDetail('entities.detail.subtitle.active-alerts');
diff --git a/web-next/components/shell/app-frame.chrome.test.tsx b/web-next/components/shell/app-frame.chrome.test.tsx
index 1a0d752486..bff8b617d5 100644
--- a/web-next/components/shell/app-frame.chrome.test.tsx
+++ b/web-next/components/shell/app-frame.chrome.test.tsx
@@ -55,6 +55,7 @@ vi.mock('@/lib/api-client', () => ({
 
 vi.mock('@/lib/app-frame-state', () => ({
   isStandaloneRoute: () => false,
+  shouldLoadHeaderRealtime: () => false,
   shouldLoadHeaderState: () => false
 }));
 
@@ -86,6 +87,20 @@ const emptySetupState = {
 };
 
 describe('app frame chrome', () => {
+  it('loads header shell primitives from a lightweight UI subpath instead of the full UI barrel', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+
+    expect(source).toContain("from '@hertzbeat/ui/shell'");
+    expect(source).not.toContain("from '@hertzbeat/ui'");
+  });
+
+  it('keeps locale menu chrome on the lightweight shell UI subpath', () => {
+    const source = readFileSync(resolve(__dirname, 'locale-option-list.tsx'), 'utf8');
+
+    expect(source).toContain("from '@hertzbeat/ui/shell'");
+    expect(source).not.toContain("from '@hertzbeat/ui'");
+  });
+
   it('does not keep the startup baseline once live header setup state resolves empty', async () => {
     const { buildSetupSummary } = await import('./app-frame');
     const summary = buildSetupSummary(emptySetupState, setupT);
diff --git a/web-next/components/shell/app-frame.tsx b/web-next/components/shell/app-frame.tsx
index 6affb5ebb3..de372105e3 100644
--- a/web-next/components/shell/app-frame.tsx
+++ b/web-next/components/shell/app-frame.tsx
@@ -38,7 +38,7 @@ import {
   type HzAiChatScheduleDeleteStatus,
   type HzAiChatScheduleRow,
   type HzAiChatScheduleStatus
-} from '@hertzbeat/ui';
+} from '@hertzbeat/ui/shell';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { LocaleOptionList } from '@/components/shell/locale-option-list';
 import { AuthGate } from '@/components/shell/auth-gate';
@@ -46,7 +46,7 @@ import { AppSidebar } from '@/components/shell/app-sidebar';
 import { PlatformCopyrightFooter } from './platform-copyright-footer';
 import { Input } from '../ui/input';
 import { apiGet } from '@/lib/api-client';
-import { isStandaloneRoute, shouldLoadHeaderState } from '@/lib/app-frame-state';
+import { isStandaloneRoute, shouldLoadHeaderRealtime, shouldLoadHeaderState } from '@/lib/app-frame-state';
 import { buildAlertListUrl } from '@/lib/alert-manage/query-state';
 import {
   AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS,
@@ -1132,7 +1132,7 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
   }, [locale, pathname, standaloneRoute, t]);
 
   useEffect(() => {
-    if (!shouldLoadHeaderState(pathname)) {
+    if (!shouldLoadHeaderRealtime(pathname)) {
       setHeaderRealtimeStatus('idle');
       setHeaderRealtimeNotice(null);
       return undefined;
diff --git a/web-next/components/shell/auth-gate.tsx b/web-next/components/shell/auth-gate.tsx
index b7784eec8d..7d397f976f 100644
--- a/web-next/components/shell/auth-gate.tsx
+++ b/web-next/components/shell/auth-gate.tsx
@@ -1,12 +1,19 @@
 'use client';
 
 import { useEffect, useState } from 'react';
+import { usePathname } from 'next/navigation';
 import { buildLoginRedirectHref, buildLoginReturnTo } from '@/lib/passport-login/controller';
-import { readClientSessionState } from '@/lib/session-client';
+import { hasClientSessionMarker, readClientSessionState } from '@/lib/session-client';
+
+function shouldOptimisticallyRenderProtectedRoute(pathname: string) {
+  return pathname === '/topology' || pathname.startsWith('/topology/');
+}
 
 export function AuthGate({ children }: { children: React.ReactNode }) {
-  const [ready, setReady] = useState(false);
-  const [authed, setAuthed] = useState(false);
+  const pathname = usePathname();
+  const optimisticRender = shouldOptimisticallyRenderProtectedRoute(pathname) && hasClientSessionMarker();
+  const [ready, setReady] = useState(optimisticRender);
+  const [authed, setAuthed] = useState(optimisticRender);
 
   useEffect(() => {
     let cancelled = false;
diff --git a/web-next/components/shell/auth-recovery.chrome.test.ts b/web-next/components/shell/auth-recovery.chrome.test.ts
index ca241c55ff..b1cc6d2227 100644
--- a/web-next/components/shell/auth-recovery.chrome.test.ts
+++ b/web-next/components/shell/auth-recovery.chrome.test.ts
@@ -26,4 +26,18 @@ describe('auth recovery posture', () => {
     expect(authGateSource).not.toContain('Restoring your workspace');
     expect(authGateSource).not.toContain('max-w-lg');
   });
+
+  it('lets topology preheat protected content when a local session marker exists while still verifying the BFF session', () => {
+    const authGateSource = readFileSync(resolve(process.cwd(), 'components/shell/auth-gate.tsx'), 'utf8');
+    const sessionClientSource = readFileSync(resolve(process.cwd(), 'lib/session-client.ts'), 'utf8');
+
+    expect(authGateSource).toContain('shouldOptimisticallyRenderProtectedRoute(pathname)');
+    expect(authGateSource).toContain('hasClientSessionMarker()');
+    expect(authGateSource).toContain("pathname === '/topology' || pathname.startsWith('/topology/')");
+    expect(authGateSource).toContain('useState(optimisticRender)');
+    expect(authGateSource).toContain('readClientSessionState().then(session =>');
+    expect(authGateSource).toContain('window.location.href = buildLoginRedirectHref');
+    expect(sessionClientSource).toContain('export function hasClientSessionMarker()');
+    expect(sessionClientSource).toContain("trimmed.startsWith('hb_ui_session=')");
+  });
 });
diff --git a/web-next/components/shell/locale-option-list.tsx b/web-next/components/shell/locale-option-list.tsx
index 0b41b3dabb..7dd2a891b6 100644
--- a/web-next/components/shell/locale-option-list.tsx
+++ b/web-next/components/shell/locale-option-list.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import { HzLocaleMenuOption } from '@hertzbeat/ui';
+import { HzLocaleMenuOption } from '@hertzbeat/ui/shell';
 import { useI18n } from '../providers/i18n-provider';
 import { cn } from '../../lib/utils';
 import type { LOCALES, LocaleCode } from '../../lib/i18n';
diff --git a/web-next/lib/app-frame-state.test.ts b/web-next/lib/app-frame-state.test.ts
index 7ad9e6cf83..26c06ca2e6 100644
--- a/web-next/lib/app-frame-state.test.ts
+++ b/web-next/lib/app-frame-state.test.ts
@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest';
-import { isActiveRoute, isStandaloneRoute, shouldLoadHeaderState } from './app-frame-state';
+import { isActiveRoute, isStandaloneRoute, shouldLoadHeaderRealtime, shouldLoadHeaderState } from './app-frame-state';
 
 describe('app frame state', () => {
   it('recognizes standalone routes', () => {
@@ -35,4 +35,13 @@ describe('app frame state', () => {
     expect(shouldLoadHeaderState('/monitors/42')).toBe(false);
     expect(shouldLoadHeaderState('/monitors/42/edit')).toBe(false);
   });
+
+  it('does not let header state or SSE streams compete with topology first-canvas inspection', () => {
+    expect(shouldLoadHeaderState('/topology')).toBe(false);
+    expect(shouldLoadHeaderState('/topology?sourceKind=cmdb-manual-label')).toBe(false);
+    expect(shouldLoadHeaderRealtime('/topology')).toBe(false);
+    expect(shouldLoadHeaderRealtime('/topology?sourceKind=cmdb-manual-label')).toBe(false);
+    expect(shouldLoadHeaderState('/overview')).toBe(true);
+    expect(shouldLoadHeaderRealtime('/overview')).toBe(true);
+  });
 });
diff --git a/web-next/lib/app-frame-state.ts b/web-next/lib/app-frame-state.ts
index 4b56d1f1fa..1b543a1ba3 100644
--- a/web-next/lib/app-frame-state.ts
+++ b/web-next/lib/app-frame-state.ts
@@ -12,5 +12,11 @@ export function isStandaloneRoute(pathname: string) {
 export function shouldLoadHeaderState(pathname: string) {
   const pathOnly = pathname.split('?')[0] || pathname;
   const isMonitorCrudRoute = pathOnly === '/monitors' || pathOnly.startsWith('/monitors/');
-  return !isStandaloneRoute(pathOnly) && !isMonitorCrudRoute;
+  const isTopologyRoute = pathOnly === '/topology' || pathOnly.startsWith('/topology/');
+  return !isStandaloneRoute(pathOnly) && !isMonitorCrudRoute && !isTopologyRoute;
+}
+
+export function shouldLoadHeaderRealtime(pathname: string) {
+  const pathOnly = pathname.split('?')[0] || pathname;
+  return shouldLoadHeaderState(pathOnly);
 }
diff --git a/web-next/lib/entity-contract.test.ts b/web-next/lib/entity-contract.test.ts
index b18d606fae..8de7436da3 100644
--- a/web-next/lib/entity-contract.test.ts
+++ b/web-next/lib/entity-contract.test.ts
@@ -133,6 +133,36 @@ describe('HertzBeat unified entity contract', () => {
     });
   });
 
+  it('prefers shared signal evidence over stale entity detail signal fields', () => {
+    expect(
+      buildEntityContractFromDetail({
+        entity: {
+          entity: {
+            id: 8,
+            name: 'payment',
+            type: 'service'
+          }
+        },
+        evidenceSummary: {
+          logHintCount: 1,
+          lastEvidenceAt: 'stale-evidence'
+        },
+        logSummary: { hintCount: 1 },
+        traceSummary: { recentTraceCount: 1, recentErrorTraceCount: 0, latestObservedAt: 'stale-trace', active: false },
+        signalEvidence: {
+          logSummary: { hintCount: 6 },
+          traceSummary: { recentTraceCount: 9, recentErrorTraceCount: 3, latestObservedAt: 'shared-trace', active: true }
+        }
+      })
+    ).toMatchObject({
+      signals: {
+        logs: { present: true, count: 6 },
+        traces: { present: true, count: 9, errorCount: 3 }
+      },
+      lastSeen: 'shared-trace'
+    });
+  });
+
   it('normalizes OTLP bound entities and monitor objects as first-class entity sources', () => {
     expect(
       buildEntityContractFromOtlpBinding({
diff --git a/web-next/lib/entity-contract.ts b/web-next/lib/entity-contract.ts
index d0d64ebb8b..5398115fa2 100644
--- a/web-next/lib/entity-contract.ts
+++ b/web-next/lib/entity-contract.ts
@@ -69,6 +69,14 @@ function compactAttributes(values: Record<string, unknown>) {
   );
 }
 
+function detailLogSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'logSummary'>) {
+  return detail.signalEvidence?.logSummary || detail.logSummary;
+}
+
+function detailTraceSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'traceSummary'>) {
+  return detail.signalEvidence?.traceSummary || detail.traceSummary;
+}
+
 function normalizeSource(source: unknown): HertzBeatEntitySourceKind {
   const value = String(source || '').toLowerCase();
 
@@ -172,8 +180,10 @@ export function buildEntityContractFromDetail(detail: EntityDetailDto): HertzBea
   const monitorBinds = entityDto.monitorBinds ?? [];
   const relations = entityDto.relations ?? [];
   const activeAlertCount = detail.evidenceSummary?.activeAlertCount ?? detail.activeAlerts?.length ?? 0;
-  const traceCount = detail.traceSummary?.recentTraceCount ?? 0;
-  const logCount = detail.logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount ?? 0;
+  const traceSummary = detailTraceSummary(detail);
+  const logSummary = detailLogSummary(detail);
+  const traceCount = traceSummary?.recentTraceCount ?? 0;
+  const logCount = logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount ?? 0;
   const monitorCount = detail.monitorSummary?.totalBoundMonitors ?? monitorBinds.length;
 
   return {
@@ -195,9 +205,9 @@ export function buildEntityContractFromDetail(detail: EntityDetailDto): HertzBea
       metrics: { present: monitorCount > 0, count: monitorCount },
       logs: { present: logCount > 0, count: logCount },
       traces: {
-        present: Boolean(detail.traceSummary?.active || traceCount > 0),
+        present: Boolean(traceSummary?.active || traceCount > 0),
         count: traceCount,
-        errorCount: detail.traceSummary?.recentErrorTraceCount ?? 0
+        errorCount: traceSummary?.recentErrorTraceCount ?? 0
       },
       alerts: { present: activeAlertCount > 0, count: activeAlertCount }
     },
@@ -207,7 +217,7 @@ export function buildEntityContractFromDetail(detail: EntityDetailDto): HertzBea
       downMonitorCount: detail.evidenceSummary?.downMonitorCount,
       healthyMonitorCount: detail.evidenceSummary?.healthyMonitorCount
     },
-    lastSeen: detail.traceSummary?.latestObservedAt ?? detail.evidenceSummary?.lastEvidenceAt ?? null
+    lastSeen: traceSummary?.latestObservedAt ?? detail.evidenceSummary?.lastEvidenceAt ?? null
   };
 }
 
diff --git a/web-next/lib/entity-detail/view-model.test.ts b/web-next/lib/entity-detail/view-model.test.ts
index 5f1d94142c..e23536e30e 100644
--- a/web-next/lib/entity-detail/view-model.test.ts
+++ b/web-next/lib/entity-detail/view-model.test.ts
@@ -64,6 +64,50 @@ describe('entity detail view model', () => {
     ]);
   });
 
+  it('prefers the shared signal evidence bundle over scattered signal fields', () => {
+    const detail = {
+      evidenceSummary: { downMonitorCount: 0 },
+      monitorSummary: { totalBoundMonitors: 1 },
+      logSummary: { hintCount: 1, preferredQueryTitle: 'stale log context' },
+      traceSummary: { recentTraceCount: 1, recentErrorTraceCount: 0, latestTraceId: 'stale-trace' },
+      unifiedEvidenceSummary: {
+        activeSignalCount: 1,
+        metricsActive: true,
+        logsActive: false,
+        tracesActive: false,
+        metricEvidenceCount: 1,
+        logEvidenceCount: 0,
+        traceEvidenceCount: 0
+      },
+      signalEvidence: {
+        logSummary: { hintCount: 5, preferredQueryTitle: 'shared log context' },
+        traceSummary: { recentTraceCount: 8, recentErrorTraceCount: 2, latestTraceId: 'shared-trace' },
+        unifiedEvidenceSummary: {
+          activeSignalCount: 3,
+          metricsActive: true,
+          logsActive: true,
+          tracesActive: true,
+          metricEvidenceCount: 2,
+          logEvidenceCount: 5,
+          traceEvidenceCount: 8
+        }
+      }
+    } as any;
+
+    expect(buildSummaryRows(detail)).toEqual([
+      { title: 'Related metrics', copy: '1 bound monitors', meta: 'No abnormal monitors', tone: 'success' },
+      { title: 'Related logs', copy: '5 query hints available', meta: 'shared log context', tone: 'warning' },
+      { title: 'Related traces', copy: '8 recent traces', meta: '2 error traces', tone: 'danger' }
+    ]);
+    expect(buildUnifiedEvidenceRows(detail)[3]).toEqual({
+      title: 'Traces / RED',
+      copy: '8 recent traces',
+      meta: '2 error traces',
+      tone: 'danger'
+    });
+    expect(buildEntityContextHandoffLinks(detail, 'last-1h')[1].copy).toContain('traceId=shared-trace');
+  });
+
   it('builds the lightweight HertzBeat service health model without heavy SLO authoring', () => {
     expect(
       buildEntityHealthModel({
diff --git a/web-next/lib/entity-detail/view-model.ts b/web-next/lib/entity-detail/view-model.ts
index ce28773cac..bf7e0f55a8 100644
--- a/web-next/lib/entity-detail/view-model.ts
+++ b/web-next/lib/entity-detail/view-model.ts
@@ -12,6 +12,18 @@ export function translateEntityDetailViewModel(key: string, params?: Translation
   return interpolate(template, params);
 }
 
+function getSignalLogSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'logSummary'>) {
+  return detail.signalEvidence?.logSummary || detail.logSummary;
+}
+
+function getSignalTraceSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'traceSummary'>) {
+  return detail.signalEvidence?.traceSummary || detail.traceSummary;
+}
+
+function getSignalUnifiedEvidenceSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'unifiedEvidenceSummary'>) {
+  return detail.signalEvidence?.unifiedEvidenceSummary || detail.unifiedEvidenceSummary;
+}
+
 type DetailRow = {
   title: string;
   copy: string;
@@ -164,14 +176,16 @@ function localizeActionText(
 }
 
 export function buildSummaryRows(
-  detail: Pick<EntityDetailDto, 'evidenceSummary' | 'monitorSummary' | 'logSummary' | 'traceSummary' | 'boundMonitors'>,
+  detail: Pick<EntityDetailDto, 'evidenceSummary' | 'monitorSummary' | 'logSummary' | 'traceSummary' | 'signalEvidence' | 'boundMonitors'>,
   t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
 ) {
   const boundMonitorCount = detail.monitorSummary?.totalBoundMonitors ?? detail.boundMonitors?.length ?? 0;
   const downMonitorCount = detail.evidenceSummary?.downMonitorCount ?? 0;
-  const logHintCount = detail.logSummary?.hintCount ?? 0;
-  const traceCount = detail.traceSummary?.recentTraceCount ?? 0;
-  const errorTraceCount = detail.traceSummary?.recentErrorTraceCount ?? 0;
+  const logSummary = getSignalLogSummary(detail);
+  const traceSummary = getSignalTraceSummary(detail);
+  const logHintCount = logSummary?.hintCount ?? 0;
+  const traceCount = traceSummary?.recentTraceCount ?? 0;
+  const errorTraceCount = traceSummary?.recentErrorTraceCount ?? 0;
 
   return [
     {
@@ -189,7 +203,7 @@ export function buildSummaryRows(
         logHintCount > 0
           ? t('entities.detail.summary.logs.available', { count: formatCount(logHintCount, t('entities.detail.summary.logs.hints')) })
           : t('entities.detail.summary.logs.no-hints'),
-      meta: detail.logSummary?.preferredQueryTitle || detail.logSummary?.fallbackSearchTerm || '-',
+      meta: logSummary?.preferredQueryTitle || logSummary?.fallbackSearchTerm || '-',
       tone: logHintCount > 0 ? 'warning' : 'neutral'
     },
     {
@@ -293,6 +307,8 @@ export function buildEntityHealthModel(
   detail: EntityDetailDto,
   t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
 ): DetailRow[] {
+  const logSummary = getSignalLogSummary(detail);
+  const traceSummary = getSignalTraceSummary(detail);
   const totalBoundMonitors = finiteNumber(
     detail.monitorSummary?.totalBoundMonitors ?? detail.boundMonitors?.length ?? detail.entity?.monitorBinds?.length,
     0
@@ -302,17 +318,17 @@ export function buildEntityHealthModel(
     0,
     Math.min(totalBoundMonitors, finiteNumber(detail.evidenceSummary?.healthyMonitorCount, totalBoundMonitors - downMonitorCount))
   );
-  const recentTraceCount = finiteNumber(detail.traceSummary?.recentTraceCount, 0);
-  const recentErrorTraceCount = finiteNumber(detail.traceSummary?.recentErrorTraceCount, 0);
+  const recentTraceCount = finiteNumber(traceSummary?.recentTraceCount, 0);
+  const recentErrorTraceCount = finiteNumber(traceSummary?.recentErrorTraceCount, 0);
   const activeAlertCount = finiteNumber(
     detail.alertSummary?.totalActiveAlerts ?? detail.activeAlerts?.length ?? detail.evidenceSummary?.activeAlertCount,
     0
   );
-  const logHintCount = finiteNumber(detail.logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount, 0);
+  const logHintCount = finiteNumber(logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount, 0);
   const availabilityRatio = totalBoundMonitors > 0 ? healthyMonitorCount / totalBoundMonitors : 0;
   const errorRate = recentTraceCount > 0 ? recentErrorTraceCount / recentTraceCount : 0;
   const recentAnomalyCount = downMonitorCount + recentErrorTraceCount + logHintCount;
-  const latency = formatLatency(detail.traceSummary, t);
+  const latency = formatLatency(traceSummary, t);
   const collectorEvidence = buildCollectorHealthEvidence({
     healthyMonitorCount,
     lastEvidenceLabel: detail.evidenceSummary?.lastEvidenceAt == null ? null : String(detail.evidenceSummary.lastEvidenceAt),
@@ -381,10 +397,10 @@ export function buildEntityHealthModel(
 }
 
 export function buildUnifiedEvidenceRows(
-  detail: Pick<EntityDetailDto, 'unifiedEvidenceSummary' | 'traceSummary'>,
+  detail: Pick<EntityDetailDto, 'unifiedEvidenceSummary' | 'traceSummary' | 'signalEvidence'>,
   t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
 ): DetailRow[] {
-  const summary = detail.unifiedEvidenceSummary;
+  const summary = getSignalUnifiedEvidenceSummary(detail);
   if (!summary) {
     return [
       {
@@ -401,7 +417,7 @@ export function buildUnifiedEvidenceRows(
   const metricEvidenceCount = finiteNumber(summary.metricEvidenceCount, 0);
   const logEvidenceCount = finiteNumber(summary.logEvidenceCount, 0);
   const traceEvidenceCount = finiteNumber(summary.traceEvidenceCount, 0);
-  const traceErrorCount = finiteNumber(detail.traceSummary?.recentErrorTraceCount, 0);
+  const traceErrorCount = finiteNumber(getSignalTraceSummary(detail)?.recentErrorTraceCount, 0);
 
   return [
     {
@@ -573,7 +589,7 @@ function buildContextParams(
   const entityId = entity.id != null ? String(entity.id) : '';
   const entityName = entity.displayName || entity.name || '';
   const serviceName = normalizeContextText(routeContext.serviceName) || entity.name || entity.displayName || '';
-  const traceSummary = detail.traceSummary as (EntityDetailDto['traceSummary'] & { latestSpanId?: string | null }) | undefined;
+  const traceSummary = getSignalTraceSummary(detail);
   const timeRange = normalizeContextText(routeContext.timeRange) || 'last-1h';
   const params = new URLSearchParams();
 
diff --git a/web-next/lib/hertzbeat-2-gap-audit.test.ts b/web-next/lib/hertzbeat-2-gap-audit.test.ts
index b55c6c6bac..cc0b2f8175 100644
--- a/web-next/lib/hertzbeat-2-gap-audit.test.ts
+++ b/web-next/lib/hertzbeat-2-gap-audit.test.ts
@@ -86,7 +86,7 @@ describe('HertzBeat 2.0 release gap audit', () => {
         '/topology marks trace-call API degradation as Greptime trace graph unavailable instead of generic topology API empty evidence',
         'Topology edge signal drilldowns prefer the selected trace-call edge sample trace/span context and preserve it in returnTo links',
         'buildTopologyApiUrl forwards depth, relation type, hide-internal, and pagination scope into the topology API query',
-        'buildTopologyApiUrl defaults service-call and otlp-trace-call reads to relationType=trace-call so trace views do not fetch node-only relation graphs',
+        'buildTopologyApiUrl defaults service-call reads to relationType=trace-call while keeping application OTLP source filters on the mixed graph read',
         '/topology exposes Browser-readable topology API request scope metadata including resolved relationType=trace-call for service-call routes',
         '/topology passes resolved relationType=trace-call into shared loading and empty states for inferred service-call routes',
         'TopologyController and EntityTopologyQueryService accept backend relation type, hide-internal, and edge pagination scope for API-backed topology graphs',
@@ -165,6 +165,8 @@ describe('HertzBeat 2.0 release gap audit', () => {
         '/topology exposes node-click, edge-click, and table-row-click selection source markers so Browser can distinguish real in-page inspection from route initial state',
         '/topology clears fallback selected-edge context during node-click inspection so the G6 canvas and drawer do not retain stale edge state after ordinary node selection',
         '/topology Browser-verifies real G6 edge click opens the relationship drawer in-page without URL navigation, remount, zoom reset, or viewport drift',
+        '/topology durable Playwright smoke verifies real G6 node inspection after wheel zoom keeps URL, zoom, position, render key, and drawer state in-page',
+        '/topology durable Playwright smoke verifies compact service-call graphs open at 1x in a 2048px fresh browser viewport instead of filling the canvas with oversized nodes',
         '/topology Browser-verifies the final M5 route matrix across default, focused, service-call, and empty routes with API-owned graph or empty states and no static fallback',
         '/topology forwards topology search context into HzTopologyG6Canvas search focus metadata',
         '/topology reuses HzTopologyG6Canvas selected-node focus action for retained investigation context',
@@ -402,7 +404,8 @@ describe('HertzBeat 2.0 release gap audit', () => {
         'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java',
         'web-next/packages/hertzbeat-ui/src/index.tsx',
         'web-next/app/ui-lab/page.tsx',
-        'web-next/lib/topology-surface/controller.ts'
+        'web-next/lib/topology-surface/controller.ts',
+        'web-next/scripts/topology-g6-browser-smoke.spec.ts'
       ])
     });
     expect(audit.gaps.find(gap => gap.code === 'greptime-otlp-query-pushdown')).toMatchObject({
@@ -411,12 +414,43 @@ describe('HertzBeat 2.0 release gap audit', () => {
         'EntityTraceQueryServiceImpl builds entity trace summary/hints from storage-owned recent rows scoped by lookback time, workspace, and canonical entity identity',
         'GreptimeTraceQueryRepository owns service-to-service trace graph RED SQL aggregation with seed service source/target scope for topology',
         'GreptimeTraceQueryRepository owns grouped trace-list SQL with total_count, LIMIT, and OFFSET for paged list views',
+        'GreptimeTraceQueryRepository owns trace overview total/error/latest activity aggregate SQL with workspace, entity, service, environment, and internal-service filters',
+        'GreptimeTraceQueryRepository owns explicit trace-id overview total/error/latest aggregate SQL with trace id, workspace, entity, service, environment, and internal-service filters',
+        'GreptimeTraceQueryRepository owns entity trace summary total/error/latest/latest-trace aggregate SQL with workspace, entity, service, environment, and internal-service filters',
         'GreptimeTraceQueryRepository owns trace-id row SQL with time, service, environment, workspace, entity identity, and internal-service filters when route/detail context is present',
-        'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples'
+        'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples',
+        'GreptimeDbDataStorage owns workspace-scoped log list count/page SQL with search, noise filters, LIMIT/OFFSET, and timestamp-desc sorting',
+        'GreptimeDbDataStorage owns workspace-scoped log overview severity-bucket aggregate SQL with search and noise filters',
+        'GreptimeDbDataStorage owns workspace-scoped log trace-coverage and hourly-trend aggregate SQL with search and noise filters'
       ]),
       sourceFiles: expect.arrayContaining([
         'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java',
-        'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java'
+        'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java',
+        'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java',
+        'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java'
+      ])
+    });
+    expect(audit.gaps.find(gap => gap.code === 'entity-evidence-read-model')).toMatchObject({
+      backendApiStatus: 'partial',
+      releaseBlocking: true,
+      evidence: expect.arrayContaining([
+        'EntitySignalEvidenceBundle carries log summary, trace summary, metric evidence, log evidence, trace evidence, log hints, trace hints, unified evidence, and triage recommendation through one shared signal evidence response',
+        'EntityDetailDto exposes EntitySignalEvidenceBundle so entity detail clients can consume the shared evidence response directly',
+        'EntityDetailObservabilityReadModelService passes EntitySignalEvidenceBundle into EntityResponseHandoffReadModelService for response handoff assembly',
+        'EntityObservabilityGatewayImpl prefers EntityResponseHandoffsRequest.signalEvidence when deriving log and trace handoff context',
+        'Entity detail frontend view-model prefers EntitySignalEvidenceBundle for log summary, trace summary, unified RED evidence, and trace handoff IDs before falling back to scattered fields',
+        'EntityDetailSurface subtitle and buildEntityContractFromDetail now prefer EntitySignalEvidenceBundle for signal presence/count/last-seen context'
+      ]),
+      sourceFiles: expect.arrayContaining([
+        'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java',
+        'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java',
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java',
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java',
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java',
+        'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java',
+        'web-next/components/pages/entity-detail-surface.tsx',
+        'web-next/lib/entity-contract.ts',
+        'web-next/lib/entity-detail/view-model.ts'
       ])
     });
 
diff --git a/web-next/lib/hertzbeat-2-gap-audit.ts b/web-next/lib/hertzbeat-2-gap-audit.ts
index f1493966a1..eaabc6cfbc 100644
--- a/web-next/lib/hertzbeat-2-gap-audit.ts
+++ b/web-next/lib/hertzbeat-2-gap-audit.ts
@@ -124,7 +124,7 @@ const functionalGaps: HertzBeat2Gap[] = [
       '/topology marks trace-call API degradation as Greptime trace graph unavailable instead of generic topology API empty evidence',
       'Topology edge signal drilldowns prefer the selected trace-call edge sample trace/span context and preserve it in returnTo links',
       'buildTopologyApiUrl forwards depth, relation type, hide-internal, and pagination scope into the topology API query',
-      'buildTopologyApiUrl defaults service-call and otlp-trace-call reads to relationType=trace-call so trace views do not fetch node-only relation graphs',
+      'buildTopologyApiUrl defaults service-call reads to relationType=trace-call while keeping application OTLP source filters on the mixed graph read',
       '/topology exposes Browser-readable topology API request scope metadata including resolved relationType=trace-call for service-call routes',
       '/topology passes resolved relationType=trace-call into shared loading and empty states for inferred service-call routes',
       'TopologyController and EntityTopologyQueryService accept backend relation type, hide-internal, and edge pagination scope for API-backed topology graphs',
@@ -189,6 +189,8 @@ const functionalGaps: HertzBeat2Gap[] = [
       '/topology exposes node-click, edge-click, and table-row-click selection source markers so Browser can distinguish real in-page inspection from route initial state',
       '/topology clears fallback selected-edge context during node-click inspection so the G6 canvas and drawer do not retain stale edge state after ordinary node selection',
       '/topology Browser-verifies real G6 edge click opens the relationship drawer in-page without URL navigation, remount, zoom reset, or viewport drift',
+      '/topology durable Playwright smoke verifies real G6 node inspection after wheel zoom keeps URL, zoom, position, render key, and drawer state in-page',
+      '/topology durable Playwright smoke verifies compact service-call graphs open at 1x in a 2048px fresh browser viewport instead of filling the canvas with oversized nodes',
       '/topology Browser-verifies the final M5 route matrix across default, focused, service-call, and empty routes with API-owned graph or empty states and no static fallback',
       '/topology forwards topology search context into HzTopologyG6Canvas search focus metadata',
       '/topology forwards environment, source, search, and group scope into HzTopologyG6Canvas filter-scope metadata',
@@ -448,9 +450,10 @@ const functionalGaps: HertzBeat2Gap[] = [
       'web-next/app/ui-lab/page.tsx',
       'web-next/lib/topology-surface/controller.ts',
       'web-next/lib/topology-surface/view-model.ts',
-      'web-next/app/topology/topology-page.tsx'
+      'web-next/app/topology/topology-page.tsx',
+      'web-next/scripts/topology-g6-browser-smoke.spec.ts'
     ],
-    nextStep: 'Move any remaining topology regression proof into durable browser fixtures and keep the live Greptime RED graph available for release smoke runs.'
+    nextStep: 'Keep the live Greptime RED graph and topology G6 browser smoke available for release regression runs while finishing the remaining cross-domain release gaps.'
   },
   {
     code: 'incident-workbench-domain',
@@ -492,7 +495,7 @@ const functionalGaps: HertzBeat2Gap[] = [
     page: '/trace/manage and /log/manage',
     backendApiStatus: 'partial',
     releaseBlocking: true,
-    currentState: 'Greptime trace and log reads exist, the topology trace-call path has timestamp, environment, seed service scope, and internal-service SQL predicates plus service graph RED aggregation, and trace list reads now pass time, service namespace, environment, workspace, canonical entity identity scope, grouping, total count, limit, and offset into the Greptime trace repository before Java fallback filtering. Trace-id list/detail reads now have a Greptime trace-row overload th [...]
+    currentState: 'Greptime trace and log reads exist, the topology trace-call path has timestamp, environment, seed service scope, and internal-service SQL predicates plus service graph RED aggregation, and trace list reads now pass time, service namespace, environment, workspace, canonical entity identity scope, grouping, total count, limit, and offset into the Greptime trace repository before Java fallback filtering. Trace overview now uses Greptime-side trace grouping for total/error [...]
     requiredClosure: [
       'Push time, workspace, entity, service, environment, trace id, pagination, and sorting into Greptime SQL.',
       'Promote frequently filtered OTLP attributes to queryable columns or tags where benchmarks require it.'
@@ -502,13 +505,21 @@ const functionalGaps: HertzBeat2Gap[] = [
       'GreptimeTraceQueryRepository owns service, service namespace, environment, workspace, entity identity, internal, and timestamp predicates for recent trace rows',
       'GreptimeTraceQueryRepository owns service-to-service trace graph RED SQL aggregation with seed service source/target scope for topology',
       'GreptimeTraceQueryRepository owns grouped trace-list SQL with total_count, LIMIT, and OFFSET for paged list views',
+      'GreptimeTraceQueryRepository owns trace overview total/error/latest activity aggregate SQL with workspace, entity, service, environment, and internal-service filters',
+      'GreptimeTraceQueryRepository owns explicit trace-id overview total/error/latest aggregate SQL with trace id, workspace, entity, service, environment, and internal-service filters',
+      'GreptimeTraceQueryRepository owns entity trace summary total/error/latest/latest-trace aggregate SQL with workspace, entity, service, environment, and internal-service filters',
       'GreptimeTraceQueryRepository owns trace-id row SQL with time, service, environment, workspace, entity identity, and internal-service filters when route/detail context is present',
       'EntityTraceQueryServiceImpl pushes canonical entity identity into trace detail row reads before Java span aggregation',
-      'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples'
+      'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples',
+      'GreptimeDbDataStorage owns workspace-scoped log list count/page SQL with search, noise filters, LIMIT/OFFSET, and timestamp-desc sorting',
+      'GreptimeDbDataStorage owns workspace-scoped log overview severity-bucket aggregate SQL with search and noise filters',
+      'GreptimeDbDataStorage owns workspace-scoped log trace-coverage and hourly-trend aggregate SQL with search and noise filters'
     ],
     sourceFiles: [
       'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java',
-      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java'
+      'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java',
+      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java',
+      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java'
     ],
     nextStep: 'Move remaining non-Greptime fallback semantics away from bounded Java samples where feasible, then re-run live trace list/detail smoke after the warehouse compile blocker is cleared.'
   },
@@ -520,21 +531,36 @@ const functionalGaps: HertzBeat2Gap[] = [
     route: '/entities/[entityId]',
     backendApiStatus: 'partial',
     releaseBlocking: true,
-    currentState: 'Entity relations and detail APIs exist, but topology, incident, and signal workspaces do not share one evidence contract.',
+    currentState: 'Entity relations and detail APIs exist, entity detail now builds and returns a shared signal evidence bundle for response handoffs instead of threading log, trace, and metric evidence as separate route-context fragments, and the entity detail frontend plus unified entity contract prefer that bundle when rendering signal summaries, RED evidence, page context, and log/trace handoff context. Topology, incident, and the remaining signal workspaces still need to consume the [...]
     requiredClosure: [
       'Return metrics, logs, traces, alerts, relations, and handoffs through one entity evidence read model.',
       'Make signal workspaces consume that contract instead of rebuilding route context locally.'
     ],
     evidence: [
       'EntityRelation has DAO and service coverage',
+      'EntitySignalEvidenceBundle carries log summary, trace summary, metric evidence, log evidence, trace evidence, log hints, trace hints, unified evidence, and triage recommendation through one shared signal evidence response',
+      'EntityDetailDto exposes EntitySignalEvidenceBundle so entity detail clients can consume the shared evidence response directly',
+      'EntityDetailObservabilityReadModelService passes EntitySignalEvidenceBundle into EntityResponseHandoffReadModelService for response handoff assembly',
+      'EntityObservabilityGatewayImpl prefers EntityResponseHandoffsRequest.signalEvidence when deriving log and trace handoff context',
+      'Entity detail frontend view-model prefers EntitySignalEvidenceBundle for log summary, trace summary, unified RED evidence, and trace handoff IDs before falling back to scattered fields',
+      'EntityDetailSurface subtitle and buildEntityContractFromDetail now prefer EntitySignalEvidenceBundle for signal presence/count/last-seen context',
       'signal workspaces still keep page-specific query and handoff builders'
     ],
     sourceFiles: [
       'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityRelation.java',
+      'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java',
+      'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java',
       'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/EntityController.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java',
+      'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java',
+      'web-next/components/pages/entity-detail-surface.tsx',
+      'web-next/lib/entity-contract.ts',
+      'web-next/lib/entity-detail/view-model.ts',
       'web-next/lib/signal-route-context.ts'
     ],
-    nextStep: 'Define the shared evidence response and migrate one signal handoff at a time.'
+    nextStep: 'Migrate topology, incident, and the remaining signal workbench handoffs to consume EntitySignalEvidenceBundle instead of rebuilding page-local query context.'
   },
   {
     code: 'ui-i18n-reuse-sweep',
diff --git a/web-next/lib/i18n-runtime-messages.ts b/web-next/lib/i18n-runtime-messages.ts
index c1e4754789..90cc378250 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -908,6 +908,11 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.view.zoom-in': 'Zoom in',
     'topology.view.zoom-out': 'Zoom out',
     'topology.view.focus-selected': 'Focus selected node',
+    'topology.edge-density.open-table': 'Open edge table',
+    'topology.edge-density.hidden-in-table': '{{count}} more in table',
+    'topology.metric-table.edge-summary': 'Canvas {{rendered}} / table {{total}}',
+    'topology.metric-table.row-render-summary': 'Showing {{rendered}} of {{total}} rows',
+    'topology.metric-table.row-render-show-more': 'Show {{next}} of {{total}} rows',
     'topology.locate-entity': 'Locate entity',
     'topology.current-filter': 'Current filter',
     'topology.all-entities': 'All entities',
@@ -930,6 +935,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.toolbar.reset': 'Reset',
     'topology.focus-trail.label': 'Focused topology',
     'topology.focus-trail.exit': 'Exit focus',
+    'topology.view-scope.focused-adjacency': 'Focused local view',
+    'topology.view-scope.focused-adjacency.copy': 'Open global topology',
     'topology.focus-trail.hidden-count': '{{count}} hidden by scope',
     'topology.focus-trail.filter.environment': 'Environment',
     'topology.focus-trail.filter.time-range': 'Time range',
@@ -944,11 +951,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.group-panel.source-kind': 'Evidence source',
     'topology.group-panel.node-count': '{{count}} visible nodes',
     'topology.group-panel.edge-count': '{{count}} visible edges',
+    'topology.g6.summary.rendered-node-count': '{{rendered}}/{{total}} nodes rendered',
+    'topology.g6.summary.rendered-edge-count': '{{rendered}}/{{total}} edges rendered',
     'topology.group-panel.collapsed-count': '{{count}} collapsed',
     'topology.group-panel.active-scope': 'active scope',
     'topology.group-panel.clear-group': 'Clear group',
     'topology.group-panel.open-table': 'Open table',
     'topology.path-summary.title': 'Selected path',
+    'topology.path-summary.title.preview': 'Path preview',
+    'topology.path-summary.title.selected': 'Selected path',
+    'topology.path-summary.title.hovered': 'Hovered path',
     'topology.path-summary.source': 'Source',
     'topology.path-summary.target': 'Target',
     'topology.path-summary.relation': 'Relation',
@@ -960,8 +972,13 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.empty.api.title': 'No topology relationships found',
     'topology.empty.api.copy': 'The API returned no entity, monitor, or trace-call relationship evidence for the current filter.',
     'topology.empty.trace-call.title': 'No trace-call topology evidence',
-    'topology.empty.trace-call.copy': 'Greptime has no OTLP trace-call edges with RED metrics for the selected time and environment. Relation nodes are hidden so this view does not look complete without trace evidence.',
+    'topology.empty.trace-call.copy': 'No trace-call edges in this window.',
     'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.relation-gap.title': 'No relationship edges returned',
+    'topology.relation-gap.copy': 'Nodes loaded, but this scope has no relation, trace-call, or ownership edges.',
+    'topology.relation-gap.action.trace-call': 'View call relationships',
+    'topology.relation-gap.action.trace-call.copy': 'Switch to OTLP trace-call evidence for this time range.',
+    'topology.node-only.explanation': 'Entities only · no relation edges',
     'topology.degraded.api.title': 'Topology API unavailable',
     'topology.degraded.api.copy': 'The topology graph is degraded because API, Greptime, trace, or relation evidence could not be loaded for the selected scope.',
     'topology.degraded.api.source': 'Topology API unavailable',
@@ -1038,6 +1055,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.metrics': 'Metrics evidence',
     'topology.edge.link.logs': 'Logs evidence',
     'topology.edge.link.traces': 'Traces evidence',
+    'topology.edge.render-window.label': 'Canvas window',
     'topology.current-entity': 'Current entity',
     'topology.current-entity.signals': 'Signals: {{signals}}',
     'topology.current-entity.boundary': 'Signal handoffs keep the current entity, time range, environment, and topology scope.',
@@ -1058,10 +1076,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.metric-table.row-action': 'Inspect',
     'topology.metric-table.open-edge-aria': 'Open topology edge {{edge}}',
     'topology.metric-table.filter.all': 'All',
-    'topology.metric-table.filter.visible': 'Visible',
-    'topology.metric-table.filter.partial': 'Partial',
-    'topology.metric-table.filter.hidden': 'Hidden',
-    'topology.metric-table.filter.unknown': 'Unknown',
+    'topology.metric-table.filter.visible': 'Both endpoints on canvas',
+    'topology.metric-table.filter.partial': 'One endpoint on canvas',
+    'topology.metric-table.filter.hidden': 'Outside canvas',
+    'topology.metric-table.filter.unknown': 'Unknown endpoints',
     'topology.red.request-rate': 'Request rate',
     'topology.red.error-rate': 'Error rate',
     'topology.red.latency-p95': 'P95 latency',
@@ -4427,6 +4445,11 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.view.zoom-in': '放大',
     'topology.view.zoom-out': '缩小',
     'topology.view.focus-selected': '聚焦选中节点',
+    'topology.edge-density.open-table': '打开边表格',
+    'topology.edge-density.hidden-in-table': '其余 {{count}} 条在表格',
+    'topology.metric-table.edge-summary': '画布 {{rendered}} / 表格 {{total}}',
+    'topology.metric-table.row-render-summary': '显示 {{rendered}} / {{total}} 条关系',
+    'topology.metric-table.row-render-show-more': '显示 {{next}} / {{total}} 条关系',
     'topology.locate-entity': '定位实体',
     'topology.current-filter': '当前筛选',
     'topology.all-entities': '全部实体',
@@ -4449,6 +4472,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.toolbar.reset': '重置',
     'topology.focus-trail.label': '聚焦拓扑',
     'topology.focus-trail.exit': '退出聚焦',
+    'topology.view-scope.focused-adjacency': '当前为局部聚焦视角',
+    'topology.view-scope.focused-adjacency.copy': '查看全局拓扑',
     'topology.focus-trail.hidden-count': '当前范围隐藏 {{count}} 个',
     'topology.focus-trail.filter.environment': '环境',
     'topology.focus-trail.filter.time-range': '时间范围',
@@ -4463,11 +4488,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.group-panel.source-kind': '证据来源',
     'topology.group-panel.node-count': '{{count}} 个可见节点',
     'topology.group-panel.edge-count': '{{count}} 条可见边',
+    'topology.g6.summary.rendered-node-count': '{{rendered}}/{{total}} 个节点已渲染',
+    'topology.g6.summary.rendered-edge-count': '{{rendered}}/{{total}} 条边已渲染',
     'topology.group-panel.collapsed-count': '折叠 {{count}} 个',
     'topology.group-panel.active-scope': '当前范围',
     'topology.group-panel.clear-group': '清除分组',
     'topology.group-panel.open-table': '打开表格',
     'topology.path-summary.title': '选中路径',
+    'topology.path-summary.title.preview': '路径预览',
+    'topology.path-summary.title.selected': '选中路径',
+    'topology.path-summary.title.hovered': '悬停路径',
     'topology.path-summary.source': '起点',
     'topology.path-summary.target': '终点',
     'topology.path-summary.relation': '关系',
@@ -4479,8 +4509,13 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.empty.api.title': '未找到拓扑关系',
     'topology.empty.api.copy': '当前筛选下 API 没有返回实体、监控对象或链路调用关系证据。',
     'topology.empty.trace-call.title': '暂无 trace 调用拓扑证据',
-    'topology.empty.trace-call.copy': 'Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。',
+    'topology.empty.trace-call.copy': '当前窗口无 trace 调用边。',
     'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.relation-gap.title': '未返回关系边',
+    'topology.relation-gap.copy': '节点已加载,但当前范围没有关系、调用或归属边证据。',
+    'topology.relation-gap.action.trace-call': '查看调用关系',
+    'topology.relation-gap.action.trace-call.copy': '切换到当前时间范围的 OTLP trace 调用证据。',
+    'topology.node-only.explanation': '仅实体 · 暂无关系边',
     'topology.degraded.api.title': '拓扑 API 不可用',
     'topology.degraded.api.copy': '当前范围无法加载 API、Greptime、链路或关系证据,拓扑图处于降级状态。',
     'topology.degraded.api.source': '拓扑 API 不可用',
@@ -4557,6 +4592,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.metrics': '指标证据',
     'topology.edge.link.logs': '日志证据',
     'topology.edge.link.traces': '链路证据',
+    'topology.edge.render-window.label': '画布窗口',
     'topology.current-entity': '当前实体',
     'topology.current-entity.signals': '信号:{{signals}}',
     'topology.current-entity.boundary': '信号跳转会保留当前实体、时间范围、环境和拓扑范围。',
@@ -4577,10 +4613,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.metric-table.row-action': '检查',
     'topology.metric-table.open-edge-aria': '打开拓扑边 {{edge}}',
     'topology.metric-table.filter.all': '全部',
-    'topology.metric-table.filter.visible': '可见',
-    'topology.metric-table.filter.partial': '半可见',
-    'topology.metric-table.filter.hidden': '隐藏',
-    'topology.metric-table.filter.unknown': '未知',
+    'topology.metric-table.filter.visible': '两端在画布',
+    'topology.metric-table.filter.partial': '一端在画布',
+    'topology.metric-table.filter.hidden': '画布外',
+    'topology.metric-table.filter.unknown': '端点未知',
     'topology.red.request-rate': '请求速率',
     'topology.red.error-rate': '错误率',
     'topology.red.latency-p95': 'P95 延迟',
diff --git a/web-next/lib/session-client.ts b/web-next/lib/session-client.ts
index 5f5a12b81f..5f8eaa2042 100644
--- a/web-next/lib/session-client.ts
+++ b/web-next/lib/session-client.ts
@@ -64,6 +64,16 @@ export function clearClientSessionMarker() {
   document.cookie = 'hb_ui_session=; Max-Age=0; path=/; SameSite=Lax';
 }
 
+export function hasClientSessionMarker() {
+  if (typeof document === 'undefined') return false;
+  return document.cookie
+    .split(';')
+    .some(segment => {
+      const trimmed = segment.trim();
+      return trimmed.startsWith('hb_ui_session=') && trimmed !== 'hb_ui_session=';
+    });
+}
+
 export async function readClientSessionState(): Promise<ClientSessionState> {
   try {
     const response = await fetch('/api/account/session', {
diff --git a/web-next/lib/topology-surface/controller.test.ts b/web-next/lib/topology-surface/controller.test.ts
index b27b42478d..3a83254568 100644
--- a/web-next/lib/topology-surface/controller.test.ts
+++ b/web-next/lib/topology-surface/controller.test.ts
@@ -1,7 +1,138 @@
 import { describe, expect, it, vi } from 'vitest';
-import { buildTopologyApiUrl, loadTopologyGraph } from './controller';
+import {
+  CMDB_TOPOLOGY_API_TIMEOUT_MS,
+  buildTopologyApiUrl,
+  hasUnresolvableTopologyFocusEntity,
+  loadTopologyGraph,
+  shouldPreservePreviousTopologyGraphDuringLoad,
+  resolveTopologyApiTimeoutMs
+} from './controller';
 
 describe('topology API controller', () => {
+  it('resolves topology timeRange into concrete API start and end bounds', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          entityId: '501',
+          environment: 'prod',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          timeRange: 'last-1h'
+        })
+      ).toBe(
+        '/topology?focusEntityId=501&depth=2&environment=prod&relationType=trace-call&start=1780218000000&end=1780221600000'
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('does not apply timeRange bounds to CMDB manual-label topology reads', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          sourceKind: 'cmdb-manual-label',
+          viewMode: 'application',
+          timeRange: 'last-1h'
+        })
+      ).toBe('/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('applies timeRange bounds to all-source topology reads so mixed trace edges cannot leak from stale windows', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          viewMode: 'application',
+          timeRange: 'last-1h'
+        })
+      ).toBe('/topology?depth=2&environment=prod&start=1780218000000&end=1780221600000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps application OTLP source filters on the mixed topology read so edge endpoints stay available for G6 filtering', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'application',
+          timeRange: 'last-1h'
+        })
+      ).toBe('/topology?depth=2&environment=prod&start=1780218000000&end=1780221600000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('uses the longer topology API timeout for CMDB manual-label reads so slow catalog queries are not aborted before retry', () => {
+    expect(
+      resolveTopologyApiTimeoutMs({
+        environment: 'prod',
+        sourceKind: 'cmdb-manual-label',
+        viewMode: 'application',
+        timeRange: 'last-1h'
+      })
+    ).toBe(CMDB_TOPOLOGY_API_TIMEOUT_MS);
+  });
+
+  it('retries a transient CMDB manual-label read once before degrading the topology surface', async () => {
+    const graph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['cmdb-manual-label'],
+      nodes: [
+        { id: '642126742338816', entityId: 642126742338816, entityName: 'Payment API', entityType: 'service' }
+      ],
+      edges: []
+    };
+    const apiGet = vi
+      .fn()
+      .mockRejectedValueOnce(new Error('transient proxy stall'))
+      .mockResolvedValueOnce(graph);
+
+    await expect(
+      loadTopologyGraph(apiGet, {
+        environment: 'prod',
+        sourceKind: 'cmdb-manual-label',
+        viewMode: 'application',
+        timeRange: 'last-1h'
+      })
+    ).resolves.toBe(graph);
+    expect(apiGet).toHaveBeenCalledTimes(2);
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label');
+  });
+
+  it('does not retry trace-call topology reads because they already use an explicit long timeout', async () => {
+    const apiGet = vi.fn().mockRejectedValue(new Error('trace query unavailable'));
+
+    await expect(
+      loadTopologyGraph(apiGet, {
+        environment: 'prod',
+        sourceKind: 'otlp-trace-call',
+        viewMode: 'service-call',
+        timeRange: 'last-1h'
+      })
+    ).rejects.toThrow('trace query unavailable');
+    expect(apiGet).toHaveBeenCalledTimes(1);
+  });
+
   it('builds the focused topology API URL from numeric entity route context', async () => {
     const apiGet = vi.fn(async () => ({
       apiBacked: true,
@@ -26,14 +157,101 @@ describe('topology API controller', () => {
     });
 
     expect(apiGet).toHaveBeenCalledWith(
-      '/topology?focusEntityId=501&depth=1&environment=prod&sourceKind=otlp-trace-call&relationType=trace-call&hideInternal=true&pageIndex=2&pageSize=50&start=1710000000000&end=1710003600000'
+      '/topology?focusEntityId=501&depth=1&environment=prod&relationType=trace-call&hideInternal=true&pageIndex=2&pageSize=50&start=1710000000000&end=1710003600000'
     );
     expect(buildTopologyApiUrl({ entityId: 'service:commerce/checkout', environment: 'prod' })).toBe(
       '/topology?depth=2&environment=prod'
     );
   });
 
-  it('defaults service-call topology API reads to trace-call relation scope', async () => {
+  it('maps the local Greptime scale proof route to the seeded focused gateway API scope', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'greptime-real'
+        })
+      ).toBe(
+        '/topology?focusEntityId=646562420231424&depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000'
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps the Greptime scale proof route on the seeded proof window after relative time moves forward', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-06-02T04:30:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'greptime-real'
+        })
+      ).toBe(
+        '/topology?focusEntityId=646562420231424&depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000'
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps the mixed star/mesh scale proof on the global Greptime API scope', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'mixed-star-mesh'
+        })
+      ).toBe('/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps the mixed star/mesh scale proof on the seeded proof window after relative time moves forward', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-06-02T04:30:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'mixed-star-mesh'
+        })
+      ).toBe('/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('marks non-numeric focused entity ids as unresolvable for the numeric topology API', () => {
+    expect(hasUnresolvableTopologyFocusEntity({ entityId: 'service:commerce/checkout' })).toBe(true);
+    expect(hasUnresolvableTopologyFocusEntity({ entityId: 'missing-topology-entity' })).toBe(true);
+    expect(hasUnresolvableTopologyFocusEntity({ entityId: '501' })).toBe(false);
+    expect(hasUnresolvableTopologyFocusEntity({})).toBe(false);
+  });
+
+  it('defaults service-call topology API reads to trace-call relation scope without hiding persisted OTLP relation evidence', async () => {
     const apiGet = vi.fn(async () => ({
       apiBacked: true,
       focusEntityId: null,
@@ -50,7 +268,7 @@ describe('topology API controller', () => {
     });
 
     expect(apiGet).toHaveBeenCalledWith(
-      '/topology?depth=2&environment=prod&sourceKind=otlp-trace-call&relationType=trace-call'
+      '/topology?depth=2&environment=prod&relationType=trace-call'
     );
     expect(buildTopologyApiUrl({ environment: 'prod', viewMode: 'service-call' })).toBe(
       '/topology?depth=2&environment=prod&relationType=trace-call'
@@ -71,7 +289,7 @@ describe('topology API controller', () => {
 
       await rejection;
       expect(apiGet).toHaveBeenCalledWith(
-        '/topology?depth=2&sourceKind=otlp-trace-call&relationType=trace-call',
+        '/topology?depth=2&relationType=trace-call',
         expect.objectContaining({ signal: expect.any(AbortSignal) })
       );
     } finally {
@@ -116,7 +334,7 @@ describe('topology API controller', () => {
 
     expect(apiGet).toHaveBeenCalledTimes(1);
     expect(apiGet).toHaveBeenCalledWith(
-      '/topology?depth=2&sourceKind=otlp-trace-call&relationType=trace-call',
+      '/topology?depth=2&relationType=trace-call',
       expect.objectContaining({ signal: expect.any(AbortSignal) })
     );
 
@@ -132,4 +350,132 @@ describe('topology API controller', () => {
     await expect(first).resolves.toMatchObject({ apiBacked: true, sourceKinds: ['otlp-trace-call'] });
     await expect(second).resolves.toMatchObject({ apiBacked: true, sourceKinds: ['otlp-trace-call'] });
   });
+
+  it('reuses a recently completed topology prefetch when the heavy page mounts after the route-client request resolved', async () => {
+    const graph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['cmdb-manual-label'],
+      nodes: [],
+      edges: []
+    };
+    const apiGet = vi.fn(async () => graph);
+
+    await expect(
+      loadTopologyGraph(apiGet, { sourceKind: 'cmdb-manual-label', viewMode: 'application' }, { timeoutMs: 60000 })
+    ).resolves.toBe(graph);
+    await expect(
+      loadTopologyGraph(apiGet, { sourceKind: 'cmdb-manual-label', viewMode: 'application' }, { timeoutMs: 60000 })
+    ).resolves.toBe(graph);
+
+    expect(apiGet).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not reuse completed topology reads for scale proof routes because Greptime proof data can be reseeded in place', async () => {
+    const staleGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['otlp-trace-call'],
+      nodes: [{ id: 'stale-node' }],
+      edges: [{ id: 'stale-edge' }]
+    };
+    const freshGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['otlp-trace-call'],
+      nodes: [{ id: 'fresh-node-a' }, { id: 'fresh-node-b' }],
+      edges: [{ id: 'fresh-edge-a' }, { id: 'fresh-edge-b' }]
+    };
+    const apiGet = vi.fn()
+      .mockResolvedValueOnce(staleGraph)
+      .mockResolvedValueOnce(freshGraph);
+    const context = {
+      environment: 'prod',
+      sourceKind: 'otlp-trace-call',
+      viewMode: 'service-call',
+      depth: '2',
+      scaleProof: 'mixed-star-mesh'
+    };
+
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000 })).resolves.toBe(staleGraph);
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000 })).resolves.toBe(freshGraph);
+
+    expect(apiGet).toHaveBeenCalledTimes(2);
+  });
+
+  it('cache-busts volatile scale proof fetches without changing the clean topology API request path', async () => {
+    const graph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['otlp-trace-call'],
+      nodes: [],
+      edges: []
+    };
+    const apiGet = vi.fn().mockResolvedValue(graph);
+    const context = {
+      environment: 'prod',
+      viewMode: 'service-call',
+      sourceKind: 'otlp-trace-call',
+      depth: '2',
+      scaleProof: 'mixed-star-mesh'
+    };
+
+    expect(buildTopologyApiUrl(context)).toBe(
+      '/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000'
+    );
+
+    await expect(loadTopologyGraph(apiGet, context, { cacheBust: () => 'proof-1' })).resolves.toBe(graph);
+
+    expect(apiGet).toHaveBeenCalledWith(
+      '/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000&_hbTopologyCacheBust=proof-1'
+    );
+  });
+
+  it('bypasses completed topology cache when an explicit refresh cache-bust is provided', async () => {
+    const staleGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['entity-relation'],
+      nodes: [{ id: 'stale-node' }],
+      edges: []
+    };
+    const freshGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['entity-relation'],
+      nodes: [{ id: 'fresh-node' }],
+      edges: []
+    };
+    const apiGet = vi.fn()
+      .mockResolvedValueOnce(staleGraph)
+      .mockResolvedValueOnce(freshGraph);
+    const context = {
+      environment: 'prod',
+      viewMode: 'application',
+      sourceKind: 'cmdb-manual-label'
+    };
+
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000 })).resolves.toBe(staleGraph);
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000, cacheBust: () => 'manual-1' })).resolves.toBe(freshGraph);
+
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label', expect.any(Object));
+    expect(apiGet).toHaveBeenNthCalledWith(
+      2,
+      '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label&_hbTopologyCacheBust=manual-1',
+      expect.any(Object)
+    );
+  });
+
+  it('does not preserve the previous graph while volatile scale proof and refresh reads are loading', () => {
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ sourceKind: 'cmdb-manual-label', viewMode: 'application' })).toBe(true);
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ scaleProof: 'mixed-star-mesh' })).toBe(false);
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ sourceKind: 'otlp-trace-call', refresh: '30' })).toBe(false);
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ sourceKind: 'otlp-trace-call', live: 'true' })).toBe(false);
+  });
 });
diff --git a/web-next/lib/topology-surface/controller.ts b/web-next/lib/topology-surface/controller.ts
index b6f05e7565..60af006b3f 100644
--- a/web-next/lib/topology-surface/controller.ts
+++ b/web-next/lib/topology-surface/controller.ts
@@ -1,18 +1,71 @@
 import type { EntityTopologyApiGraph, TopologyRouteContext } from './view-model';
+import { resolveTimeContextBounds } from '../time-context';
 
 export type TopologyApiGet = <T>(path: string, init?: RequestInit) => Promise<T>;
 
 export type LoadTopologyGraphOptions = {
   timeoutMs?: number;
+  cacheBust?: () => string;
 };
 
+type TopologyApiMessagePayload<T> = {
+  code?: number;
+  msg?: string;
+  data?: T;
+};
+
+export const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;
+export const CMDB_TOPOLOGY_API_TIMEOUT_MS = 60000;
+export const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;
+const COMPLETED_TOPOLOGY_PREFETCH_CACHE_MS = 60000;
+const TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_FOCUS_ENTITY_ID = '646562420231424';
+const TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_API_POLICY = 'focused-real-greptime-scale-fixture';
+const TOPOLOGY_MIXED_STAR_MESH_SCALE_PROOF_API_POLICY = 'global-real-greptime-mixed-star-mesh-fixture';
+const TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_START_MS = '1780344000000';
+const TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_END_MS = '1780352700000';
+
 const inFlightTopologyGraphReads = new Map<string, Promise<EntityTopologyApiGraph>>();
+const completedTopologyGraphReads = new Map<string, { graph: EntityTopologyApiGraph; expiresAt: number }>();
+let volatileTopologyRequestSequence = 0;
 
 function normalizeNumericId(value: string | undefined) {
   const trimmed = value?.trim();
   return trimmed && /^\d+$/.test(trimmed) ? trimmed : undefined;
 }
 
+export function resolveTopologyScaleProofFocusEntityId(context: TopologyRouteContext = {}) {
+  const proof = context.scaleProof?.trim();
+  if (proof === 'greptime-real') return TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_FOCUS_ENTITY_ID;
+  return undefined;
+}
+
+export function resolveTopologyScaleProofApiPolicy(context: TopologyRouteContext = {}) {
+  const proof = context.scaleProof?.trim();
+  if (proof === 'greptime-real') return TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_API_POLICY;
+  if (proof === 'mixed-star-mesh') return TOPOLOGY_MIXED_STAR_MESH_SCALE_PROOF_API_POLICY;
+  return undefined;
+}
+
+export function resolveTopologyScaleProofTimeWindow(context: TopologyRouteContext = {}) {
+  const proof = context.scaleProof?.trim();
+  if (proof !== 'greptime-real' && proof !== 'mixed-star-mesh') return undefined;
+  if (normalizeNumericId(context.start) || normalizeNumericId(context.end)) return undefined;
+  return {
+    start: TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_START_MS,
+    end: TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_END_MS,
+    policy: 'fixed-seeded-greptime-window'
+  } as const;
+}
+
+function resolveTopologyApiFocusEntityId(context: TopologyRouteContext) {
+  return normalizeNumericId(context.entityId) ?? resolveTopologyScaleProofFocusEntityId(context);
+}
+
+export function hasUnresolvableTopologyFocusEntity(context: TopologyRouteContext = {}) {
+  const trimmed = context.entityId?.trim();
+  return Boolean(trimmed && !normalizeNumericId(trimmed));
+}
+
 function normalizeBooleanText(value: string | undefined) {
   const trimmed = value?.trim().toLowerCase();
   return trimmed === 'true' || trimmed === 'false' ? trimmed : undefined;
@@ -23,62 +76,225 @@ function appendContextParam(params: URLSearchParams, key: string, value: string
   if (trimmed) params.set(key, trimmed);
 }
 
+function resolveTopologyApiTimeBounds(context: TopologyRouteContext) {
+  const explicitStart = normalizeNumericId(context.start);
+  const explicitEnd = normalizeNumericId(context.end);
+  if (explicitStart && explicitEnd) return { start: explicitStart, end: explicitEnd };
+  const scaleProofWindow = resolveTopologyScaleProofTimeWindow(context);
+  if (scaleProofWindow) {
+    return {
+      start: scaleProofWindow.start,
+      end: scaleProofWindow.end
+    };
+  }
+
+  const resolved = resolveTimeContextBounds({
+    timeRange: context.timeRange,
+    from: context.from,
+    to: context.to,
+    start: context.start,
+    end: context.end,
+    refresh: context.refresh,
+    live: context.live,
+    tz: context.tz,
+    timezone: context.timezone
+  });
+  return {
+    start: explicitStart ?? resolved?.start,
+    end: explicitEnd ?? resolved?.end
+  };
+}
+
 export function resolveTopologyRelationType(context: TopologyRouteContext) {
   const explicitRelationType = context.relationType?.trim();
   if (explicitRelationType) return explicitRelationType;
-  if (context.sourceKind === 'otlp-trace-call' || context.viewMode === 'service-call') return 'trace-call';
+  if (context.viewMode === 'service-call') return 'trace-call';
   return undefined;
 }
 
+export function resolveTopologyApiTimeoutMs(routeContext?: TopologyRouteContext) {
+  if (routeContext?.sourceKind === 'otlp-trace-call' || routeContext?.viewMode === 'service-call') {
+    return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;
+  }
+  if (routeContext?.sourceKind === 'cmdb-manual-label') {
+    return CMDB_TOPOLOGY_API_TIMEOUT_MS;
+  }
+  return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;
+}
+
+function shouldApplyTopologyApiTimeBounds(context: TopologyRouteContext, relationType: string | undefined) {
+  const hasTimeScope = Boolean(
+    context.timeRange?.trim() ||
+      context.from?.trim() ||
+      context.to?.trim() ||
+      context.start?.trim() ||
+      context.end?.trim() ||
+      context.live?.trim() ||
+      context.refresh?.trim()
+  );
+  return (
+    context.sourceKind === 'otlp-trace-call' ||
+    context.viewMode === 'service-call' ||
+    relationType === 'trace-call' ||
+    (!context.sourceKind && hasTimeScope)
+  );
+}
+
+function resolveTopologyApiRetryCount(context: TopologyRouteContext, relationType: string | undefined) {
+  if (context.sourceKind === 'cmdb-manual-label' && !shouldApplyTopologyApiTimeBounds(context, relationType)) return 1;
+  return 0;
+}
+
+function shouldUseCompletedTopologyGraphCache(context: TopologyRouteContext) {
+  if (context.scaleProof?.trim()) return false;
+  if (context.refresh?.trim()) return false;
+  if (context.live?.trim() === 'true') return false;
+  return true;
+}
+
+export function shouldPreservePreviousTopologyGraphDuringLoad(context: TopologyRouteContext = {}) {
+  return shouldUseCompletedTopologyGraphCache(context);
+}
+
+function shouldForwardTopologyApiSourceKind(context: TopologyRouteContext) {
+  const relationType = resolveTopologyRelationType(context);
+  if (context.sourceKind === 'otlp-trace-call' && relationType === 'trace-call') {
+    return false;
+  }
+  return !(
+    context.sourceKind === 'otlp-trace-call' &&
+    context.viewMode !== 'service-call' &&
+    !context.relationType?.trim()
+  );
+}
+
 export function buildTopologyApiUrl(context: TopologyRouteContext = {}) {
   const params = new URLSearchParams();
-  const focusEntityId = normalizeNumericId(context.entityId);
+  const focusEntityId = resolveTopologyApiFocusEntityId(context);
+  const relationType = resolveTopologyRelationType(context);
+  const timeBounds = shouldApplyTopologyApiTimeBounds(context, relationType)
+    ? resolveTopologyApiTimeBounds(context)
+    : {};
   if (focusEntityId) params.set('focusEntityId', focusEntityId);
   params.set('depth', normalizeNumericId(context.depth) ?? '2');
   appendContextParam(params, 'environment', context.environment);
-  appendContextParam(params, 'sourceKind', context.sourceKind);
-  appendContextParam(params, 'relationType', resolveTopologyRelationType(context));
+  appendContextParam(params, 'sourceKind', shouldForwardTopologyApiSourceKind(context) ? context.sourceKind : undefined);
+  appendContextParam(params, 'relationType', relationType);
   appendContextParam(params, 'hideInternal', normalizeBooleanText(context.hideInternal));
   appendContextParam(params, 'pageIndex', normalizeNumericId(context.pageIndex));
   appendContextParam(params, 'pageSize', normalizeNumericId(context.pageSize));
-  appendContextParam(params, 'start', normalizeNumericId(context.start));
-  appendContextParam(params, 'end', normalizeNumericId(context.end));
+  appendContextParam(params, 'start', timeBounds.start);
+  appendContextParam(params, 'end', timeBounds.end);
   return `/topology?${params.toString()}`;
 }
 
+function buildTopologyApiFetchUrl(url: string, useCompletedCache: boolean, explicitCacheBust: string | undefined) {
+  if (useCompletedCache) return url;
+  const [pathname, query = ''] = url.split('?');
+  const params = new URLSearchParams(query);
+  volatileTopologyRequestSequence += 1;
+  params.set('_hbTopologyCacheBust', explicitCacheBust || `${Date.now()}-${volatileTopologyRequestSequence}`);
+  return `${pathname}?${params.toString()}`;
+}
+
 export async function loadTopologyGraph(
   apiGet: TopologyApiGet,
   context: TopologyRouteContext = {},
   options: LoadTopologyGraphOptions = {}
 ) {
   const url = buildTopologyApiUrl(context);
+  const relationType = resolveTopologyRelationType(context);
+  const retryCount = resolveTopologyApiRetryCount(context, relationType);
   const timeoutMs = options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 0;
-  const inFlightKey = `${url}::${timeoutMs}`;
+  const explicitCacheBust = options.cacheBust?.().trim() || undefined;
+  const useCompletedCache = shouldUseCompletedTopologyGraphCache(context) && !explicitCacheBust;
+  const fetchUrl = buildTopologyApiFetchUrl(url, useCompletedCache, explicitCacheBust);
+  const inFlightKey = `${url}::${timeoutMs}::${retryCount}${explicitCacheBust ? `::${explicitCacheBust}` : ''}`;
   const inFlight = inFlightTopologyGraphReads.get(inFlightKey);
   if (inFlight) return inFlight;
+  if (useCompletedCache) {
+    const completed = completedTopologyGraphReads.get(inFlightKey);
+    if (completed) {
+      if (completed.expiresAt > Date.now()) return completed.graph;
+      completedTopologyGraphReads.delete(inFlightKey);
+    }
+  } else {
+    completedTopologyGraphReads.delete(inFlightKey);
+  }
 
   const request = (async () => {
-    if (!timeoutMs) return apiGet<EntityTopologyApiGraph>(url);
-
-    const controller = new AbortController();
-    const apiRequest = apiGet<EntityTopologyApiGraph>(url, { signal: controller.signal });
-    let timeoutId: ReturnType<typeof setTimeout> | undefined;
-    const timeout = new Promise<never>((_, reject) => {
-      timeoutId = setTimeout(() => {
-        controller.abort();
-        reject(new Error(`Topology API request timed out after ${timeoutMs}ms`));
-      }, timeoutMs);
-    });
-
-    try {
-      return await Promise.race([apiRequest, timeout]);
-    } finally {
-      if (timeoutId) clearTimeout(timeoutId);
+    const readOnce = () => {
+      if (!timeoutMs) return apiGet<EntityTopologyApiGraph>(fetchUrl);
+
+      const controller = new AbortController();
+      const apiRequest = apiGet<EntityTopologyApiGraph>(fetchUrl, { signal: controller.signal });
+      let timeoutId: ReturnType<typeof setTimeout> | undefined;
+      const timeout = new Promise<never>((_, reject) => {
+        timeoutId = setTimeout(() => {
+          controller.abort();
+          reject(new Error(`Topology API request timed out after ${timeoutMs}ms`));
+        }, timeoutMs);
+      });
+
+      return Promise.race([apiRequest, timeout]).finally(() => {
+        if (timeoutId) clearTimeout(timeoutId);
+      });
+    };
+
+    let lastError: unknown;
+    for (let attempt = 0; attempt <= retryCount; attempt += 1) {
+      try {
+        return await readOnce();
+      } catch (error) {
+        lastError = error;
+      }
+    }
+    throw lastError;
+  })().then(graph => {
+    if (useCompletedCache) {
+      completedTopologyGraphReads.set(inFlightKey, {
+        graph,
+        expiresAt: Date.now() + COMPLETED_TOPOLOGY_PREFETCH_CACHE_MS
+      });
+    } else {
+      completedTopologyGraphReads.delete(inFlightKey);
     }
-  })().finally(() => {
+    return graph;
+  }).finally(() => {
     inFlightTopologyGraphReads.delete(inFlightKey);
   });
 
   inFlightTopologyGraphReads.set(inFlightKey, request);
   return request;
 }
+
+async function fetchTopologyApiMessage<T>(path: string, init: RequestInit = {}) {
+  const locale = typeof window === 'undefined'
+    ? null
+    : window.localStorage.getItem('hb.lang') || window.localStorage.getItem('layout.lang');
+  const response = await fetch(path.startsWith('/api') ? path : `/api${path}`, {
+    ...init,
+    headers: {
+      ...(locale ? { 'Accept-Language': locale } : {}),
+      ...((init.headers as Record<string, string> | undefined) || {})
+    },
+    credentials: 'same-origin',
+    cache: 'no-store'
+  });
+  if (!response.ok) {
+    throw new Error(`Topology API request failed: ${response.status}`);
+  }
+  const payload = await response.json() as TopologyApiMessagePayload<T>;
+  if (payload.code !== 0) {
+    throw new Error(payload.msg || 'Topology API request failed');
+  }
+  return payload.data as T;
+}
+
+export function preloadTopologyGraph(
+  context: TopologyRouteContext = {},
+  options: LoadTopologyGraphOptions = {}
+) {
+  if (typeof window === 'undefined') return undefined;
+  return loadTopologyGraph(fetchTopologyApiMessage, context, options).catch(() => undefined);
+}
diff --git a/web-next/lib/topology-surface/query-state.test.ts b/web-next/lib/topology-surface/query-state.test.ts
index de13211c13..abf256ff66 100644
--- a/web-next/lib/topology-surface/query-state.test.ts
+++ b/web-next/lib/topology-surface/query-state.test.ts
@@ -24,6 +24,8 @@ describe('topology query state', () => {
         viewMode: ['resource-dependency', 'service-call'],
         sourceKind: ['database-middleware-connection', 'alert-impact'],
         groupBy: ['source-kind', 'environment'],
+        scaleProof: ['greptime-real', 'ignored-scale-proof'],
+        search: ['Service 420', 'ignored-search'],
         edgeId: ['svc-checkout--res-orders-db', 'ignored-edge'],
         returnTo: ['/alert?status=firing&returnLabel=告警']
       })
@@ -47,6 +49,8 @@ describe('topology query state', () => {
       viewMode: 'resource-dependency',
       sourceKind: 'database-middleware-connection',
       groupBy: 'source-kind',
+      scaleProof: 'greptime-real',
+      search: 'Service 420',
       edgeId: 'svc-checkout--res-orders-db',
       returnTo: '/alert?status=firing'
     });
diff --git a/web-next/lib/topology-surface/query-state.ts b/web-next/lib/topology-surface/query-state.ts
index 44813eed93..c1b200d8b9 100644
--- a/web-next/lib/topology-surface/query-state.ts
+++ b/web-next/lib/topology-surface/query-state.ts
@@ -19,6 +19,8 @@ export function readTopologyRouteContext(searchParams: TopologySearchParams = {}
     viewMode: firstParam(searchParams.viewMode),
     sourceKind: firstParam(searchParams.sourceKind),
     groupBy: firstParam(searchParams.groupBy),
+    scaleProof: firstParam(searchParams.scaleProof),
+    search: firstParam(searchParams.search),
     edgeId: firstParam(searchParams.edgeId)
   };
 }
diff --git a/web-next/lib/topology-surface/view-model.test.ts b/web-next/lib/topology-surface/view-model.test.ts
index ccfa092a9d..0ad883784a 100644
--- a/web-next/lib/topology-surface/view-model.test.ts
+++ b/web-next/lib/topology-surface/view-model.test.ts
@@ -670,7 +670,8 @@ describe('topology surface config', () => {
       focus: 'context-muted'
     });
     expect(model.nodes.find(node => node.id === 'svc-checkout')).toMatchObject({ focus: 'related' });
-    expect(model.nodes.find(node => node.id === 'svc-frontend')).toMatchObject({ focus: 'related' });
+    expect(model.nodes.find(node => node.id === 'res-orders-db')).toMatchObject({ focus: 'related' });
+    expect(model.nodes.find(node => node.id === 'svc-frontend')).toMatchObject({ focus: 'dimmed' });
     expect(model.nodes.find(node => node.id === 'k8s-checkout-workload')).toMatchObject({ focus: 'dimmed' });
   });
 
diff --git a/web-next/lib/topology-surface/view-model.ts b/web-next/lib/topology-surface/view-model.ts
index 6da093089e..093203563c 100644
--- a/web-next/lib/topology-surface/view-model.ts
+++ b/web-next/lib/topology-surface/view-model.ts
@@ -242,6 +242,8 @@ export type TopologyRouteContext = SignalRouteContext & {
   hideInternal?: string;
   pageIndex?: string;
   pageSize?: string;
+  scaleProof?: string;
+  search?: string;
 };
 
 type EntityTopologyApiRedMetrics = Partial<Record<keyof TopologyRedMetrics, number | string | null | undefined>>;
@@ -622,7 +624,7 @@ function edgeMatchesViewMode(edge: TopologyServiceEdgeSeed, viewMode: TopologyVi
 
 function edgeMatchesSource(edge: TopologyServiceEdgeSeed, sourceKind: TopologySourceKind | undefined) {
   if (!sourceKind) return true;
-  if (sourceKind === 'alert-impact') return edge.alertImpact !== 'none';
+  if (sourceKind === 'alert-impact') return edge.source === 'alert-impact' || (edge.source !== 'otlp-trace-call' && edge.alertImpact !== 'none');
   return edge.source === sourceKind;
 }
 
diff --git a/web-next/lib/types.ts b/web-next/lib/types.ts
index cfbb6e2c3f..675f19b66b 100644
--- a/web-next/lib/types.ts
+++ b/web-next/lib/types.ts
@@ -510,12 +510,25 @@ export interface EntityDetailDto {
   logSummary?: EntityLogSummary;
   traceSummary?: EntityTraceSummary;
   unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary;
+  signalEvidence?: EntitySignalEvidenceBundle;
   boundMonitors?: Monitor[];
   activeAlerts?: unknown[];
   nextActions?: EntityNextAction[];
   noiseControlSummary?: EntityNoiseControlSummary;
 }
 
+export interface EntitySignalEvidenceBundle {
+  logSummary?: EntityLogSummary;
+  traceSummary?: EntityTraceSummary & { latestSpanId?: string | null };
+  metricEvidence?: unknown[];
+  logEvidence?: unknown[];
+  traceEvidence?: unknown[];
+  logQueryHints?: unknown[];
+  traceQueryHints?: unknown[];
+  unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary;
+  triageRecommendation?: unknown;
+}
+
 export interface EntityUnifiedEvidenceSummary {
   activeSignalCount?: number;
   metricsActive?: boolean;
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index 4de18901b4..223bf42d3e 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -4497,6 +4497,8 @@ describe('@hertzbeat/ui', () => {
           totalNodeCount: 500,
           renderedNodeCount: 200,
           hiddenNodeCount: 300,
+          totalEdgeCount: 2,
+          renderedEdgeCount: 1,
           visibleNodeBudget: 200,
           tableCompanion: 'required',
           priorityNodeIds: ['payment-api'],
@@ -4508,6 +4510,7 @@ describe('@hertzbeat/ui', () => {
           errorRate: 'error ratio',
           latencyP95: 'p95 latency',
           rowAction: 'Inspect edge',
+          renderWindowEdgeSummary: 'Canvas renders 1 of 2 table edges',
           rowAriaLabel: row => `Inspect relation ${row.id}`
         }}
         rows={[
@@ -4549,6 +4552,7 @@ describe('@hertzbeat/ui', () => {
 
     expect(html).toContain('data-hz-ui="topology-metric-table"');
     expect(html).toContain('data-hz-topology-primitive="metric-table"');
+    expect(html).toContain('data-hz-topology-metric-table-root="true"');
     expect(html).toContain('data-hz-topology-metric-table-density="graph-first"');
     expect(html).toContain('data-hz-topology-metric-table-density-owner="hertzbeat-ui-metric-table-density"');
     expect(html).toContain('data-hz-topology-metric-table-visual-weight="low-interruption"');
@@ -4559,12 +4563,24 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-metric-table-title-owner="hertzbeat-ui-metric-table-title"');
     expect(html).toContain('data-hz-topology-metric-table-count-owner="hertzbeat-ui-metric-table-count"');
     expect(html).toContain('data-hz-topology-metric-rows="2"');
+    expect(html).toContain('data-hz-topology-metric-table-total-rows="2"');
     expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"');
+    expect(html).toContain(
+      'data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"'
+    );
     expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="500"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-node-count="200"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-hidden-node-count="300"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-total-edge-count="2"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-edge-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-count-policy="canvas-rendered-vs-table-total"');
+    expect(html).toContain('data-hz-topology-metric-table-canvas-rendered-edge-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-table-edge-count="2"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-summary-owner="hertzbeat-ui-metric-table-edge-summary"');
+    expect(html).toContain('Canvas renders 1 of 2 table edges');
     expect(html).toContain('data-hz-topology-metric-table-render-window-visible-node-budget="200"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-visible-node-count="2"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
@@ -4575,16 +4591,24 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-metric-table-unknown-row-count="0"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="2"');
     expect(html).toContain('data-hz-topology-metric-table-filtered-out-row-count="0"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="all"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
     expect(html).toContain('data-hz-topology-metric-table-filter-active="true"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-active="true"');
     expect(html).toContain('data-hz-topology-metric-table-filter-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-mode="table-row-click-drawer"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-edge-row="checkout-payment"');
     expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="visible"');
     expect(html).toContain('data-hz-topology-edge-row-source-node-id="checkout-api"');
@@ -4595,6 +4619,9 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="partial"');
     expect(html).toContain('data-hz-topology-edge-row-source-visible="true"');
     expect(html).toContain('data-hz-topology-edge-row-target-visible="false"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context="partial"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-source-visible="true"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-target-visible="false"');
     expect(html).toContain('data-hz-topology-edge-selected="true"');
     expect(html).toContain('aria-current="true"');
     expect(html).toContain('data-hz-topology-metric-table-endpoints-owner="hertzbeat-ui-metric-table-endpoints"');
@@ -4632,6 +4659,7 @@ describe('@hertzbeat/ui', () => {
     const html = renderToStaticMarkup(
       <HzTopologyMetricTable
         title="Topology edges"
+        selectedRowId="payment-db"
         renderWindowFilter="partial"
         renderWindowCompanion={{
           mode: 'windowed',
@@ -4642,6 +4670,9 @@ describe('@hertzbeat/ui', () => {
           tableCompanion: 'required',
           renderedNodeIds: ['checkout-api', 'payment-api']
         }}
+        labels={{
+          renderWindowFilterPartial: 'One endpoint on canvas'
+        }}
         rows={[
           {
             id: 'checkout-payment',
@@ -4680,10 +4711,81 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-metric-table-filtered-out-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toMatch(/data-hz-topology-metric-table-filter-control="partial"[^>]+data-hz-topology-metric-table-filter-active="true"/);
+    expect(html).toMatch(/data-hz-topology-metric-table-filter-control="partial"[^>]+data-hz-topology-metric-table-filter-control-active="true"/);
     expect(html).toContain('data-hz-topology-edge-row="payment-db"');
+    expect(html).toContain('data-hz-topology-edge-selected="true"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-owner="hertzbeat-ui-metric-table-row-window-context"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context="partial"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-source-visible="true"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-target-visible="false"');
+    expect(html).toContain('One endpoint on canvas');
     expect(html).not.toContain('data-hz-topology-edge-row="checkout-payment"');
   });
 
+  it('budgets dense windowed topology metric table rows without losing counts or selected context', () => {
+    const rows = Array.from({ length: 150 }, (_, index) => ({
+      id: `edge-${index}`,
+      sourceNodeId: index % 2 === 0 ? `rendered-${index}` : `hidden-${index}`,
+      targetNodeId: `hidden-target-${index}`,
+      source: `source-${index}`,
+      target: `target-${index}`,
+      relationType: 'trace-call',
+      sourceKind: 'otlp-trace-call',
+      requestRatePerSecond: 1 + index,
+      requestCount: 10 + index,
+      errorRate: 0,
+      latencyP95Ms: 40 + index,
+      tone: 'neutral' as const
+    }));
+    const html = renderToStaticMarkup(
+      <HzTopologyMetricTable
+        title="Topology edges"
+        selectedRowId="edge-149"
+        rows={rows}
+        renderWindowCompanion={{
+          mode: 'windowed',
+          totalNodeCount: 500,
+          renderedNodeCount: 200,
+          hiddenNodeCount: 300,
+          totalEdgeCount: 150,
+          renderedEdgeCount: 80,
+          visibleNodeBudget: 200,
+          tableCompanion: 'required',
+          renderedNodeIds: rows.slice(0, 80).map(row => row.sourceNodeId ?? '')
+        }}
+      />
+    );
+
+    const renderedRows = html.match(/data-hz-topology-edge-row="/g) ?? [];
+    expect(html).toContain('data-hz-topology-metric-rows="150"');
+    expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="150"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-policy="windowed-dom-budget"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-budget="120"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-page="1"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-next-count="150"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-can-show-more="true"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-count="70"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-owner="hertzbeat-ui-metric-table-hidden-row-proof"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof="available"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-filter="hidden"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-count="70"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-row-count="121"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-hidden-row-count="29"');
+    expect(renderedRows).toHaveLength(121);
+    expect(html).toContain('data-hz-topology-edge-row="edge-0"');
+    expect(html).toContain('data-hz-topology-edge-row="edge-119"');
+    expect(html).not.toContain('data-hz-topology-edge-row="edge-120"');
+    expect(html).toContain('data-hz-topology-edge-row="edge-149"');
+    expect(html).toContain('data-hz-topology-edge-selected="true"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-summary-owner="hertzbeat-ui-metric-table-row-render-summary"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action-owner="hertzbeat-ui-metric-table-row-render-action"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action="show-more"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action-selection-policy="preserve-selected-edge"');
+    expect(html).toContain('Showing 121 of 150 rows');
+    expect(html).toContain('Show 150 of 150 rows');
+  });
+
   it('renders a shared topology companion rail for table, timeline, and drawer layout ownership', () => {
     const html = renderToStaticMarkup(
       <HzTopologyCompanionRail
@@ -4885,8 +4987,8 @@ describe('@hertzbeat/ui', () => {
           sourceSlot={<div data-demo-section="source">Sources</div>}
         />
         <HzTopologyWorkbenchGrid data-testid="topology-workbench-grid">
-          <section data-demo-section="canvas">Canvas</section>
-          <section data-demo-section="companion">Companion</section>
+          <HzTopologyWorkbenchSlot kind="canvas" data-demo-section="canvas">Canvas</HzTopologyWorkbenchSlot>
+          <HzTopologyWorkbenchSlot kind="companion" data-demo-section="companion">Companion</HzTopologyWorkbenchSlot>
         </HzTopologyWorkbenchGrid>
       </HzTopologyWorkbenchFrame>
     );
@@ -4924,6 +5026,9 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-primitive="workbench-grid"');
     expect(html).toContain('data-hz-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
     expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('lg:grid-cols-[minmax(0,1fr)_320px]');
+    expect(html).toContain('lg:sticky');
+    expect(html).toContain('lg:top-[64px]');
     expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness="sticky-with-companion"');
     expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness-owner="hertzbeat-ui-workbench-grid-canvas-stickiness"');
     expect(html).toContain('data-demo-section="canvas"');
@@ -5131,6 +5236,7 @@ describe('@hertzbeat/ui', () => {
   });
 
   it('renders a compact topology legend for health, source, confidence, and stale evidence semantics', () => {
+    const source = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <HzTopologyLegend
         title="Topology legend"
@@ -5187,6 +5293,8 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('Topology legend');
     expect(html).toContain('Current evidence');
+    expect(source).not.toContain("swatch?: 'line'");
+    expect(source).not.toContain('data-hz-topology-legend-swatch-owner');
     expect(html).not.toContain('rounded-[16px]');
     expect(html).not.toContain('rounded-[14px]');
     expect(html).not.toContain('rounded-[12px]');
@@ -5208,7 +5316,7 @@ describe('@hertzbeat/ui', () => {
 	                label: 'Service node',
 	                iconSrc: 'data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%3E%3C/svg%3E',
 	                iconLibrary: 'lucide-react',
-	                iconName: 'component',
+	                iconName: 'server-cog',
 	                iconSource: 'entity-type-catalog',
 	                visualSource: 'lucide-react'
 	              },
@@ -5241,7 +5349,7 @@ describe('@hertzbeat/ui', () => {
 	    expect(html).toContain('data-hz-topology-legend-source-label="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-icon-owner="hertzbeat-ui-legend-source-icon"');
 	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
 	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
 	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
 	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
@@ -5338,6 +5446,13 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-detail-fact-density="compressed"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="edge:checkout-orders:svc-checkout:db-orders:trace-call:otlp-trace-call:unknown"');
     expect(html).toContain('data-hz-topology-detail-surface="framed"');
     expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
     expect(html).toContain('data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"');
@@ -5414,6 +5529,16 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="node:svc-checkout:none:none:unknown:otlp-trace-call:service"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement="header-dock"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement-owner="hertzbeat-ui-detail-signal-action-placement"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-sticky="top-with-header-context"');
     expect(html).toContain('data-hz-topology-detail-surface="flush"');
     expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
     expect(html).toContain('data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"');
@@ -5446,6 +5571,12 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-detail-signal-action="metrics"');
     expect(html).toContain('data-hz-topology-detail-signal-action="logs"');
     expect(html).toContain('data-hz-topology-detail-signal-action="traces"');
+    expect(html.indexOf('data-hz-topology-detail-actions="signals"')).toBeLessThan(
+      html.indexOf('data-hz-topology-detail-boundary="context"')
+    );
+    expect(html.indexOf('data-hz-topology-detail-actions="signals"')).toBeLessThan(
+      html.indexOf('data-hz-topology-detail-fact-group-owner="hertzbeat-ui-detail-fact-group"')
+    );
   });
 
   it('renders a compact topology filter strip for source and focus controls', () => {
@@ -5872,6 +6003,10 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
     expect(html).toContain('data-hz-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"');
     expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="checkout-orders"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-selected-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-hovered-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-source-id="svc-checkout"');
@@ -6004,6 +6139,26 @@ describe('@hertzbeat/ui', () => {
     expect(html).not.toContain('rounded-[12px]');
   });
 
+  it('renders canvas topology empty states without an extra framed card surface', () => {
+    const html = renderToStaticMarkup(
+      <HzTopologyEmptyState
+        title="No topology evidence"
+        copy="No trace edges were found."
+        sourceLabel="Greptime trace graph"
+        timeScope="last-1h"
+        boundary="canvas"
+        placement="canvas-center"
+        copyVisibility="assistive"
+      />
+    );
+
+    expect(html).toContain('data-hz-topology-empty-boundary="canvas"');
+    expect(html).toContain('data-hz-topology-empty-boundary-visual="frameless-canvas"');
+    expect(html).toContain('bg-transparent');
+    expect(html).toContain('shadow-none');
+    expect(html).not.toContain('border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]');
+  });
+
   it('renders a compact topology loading state owned by the shared UI lab primitive', () => {
     const html = renderToStaticMarkup(
       <HzTopologyLoadingState
@@ -6101,11 +6256,13 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
     expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
     expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
-    expect(html).toContain('[grid-template-columns:132px_minmax(220px,1fr)_132px_auto]');
+    expect(html).toContain('data-hz-topology-toolbar-control-flow="single-grid-row"');
+    expect(html).toContain('data-hz-topology-toolbar-empty-offset="none"');
+    expect(html).toContain('[grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]');
     expect(html).not.toContain('overflow-x-auto px-4 py-1');
     expect(html).toContain('overflow-x-auto px-0 py-1');
-    expect(html).toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
-    expect(html).not.toContain('h-7 !pl-2 !pr-1.5');
+    expect(html).toContain('h-7 !gap-1.5 !px-2');
+    expect(html).not.toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
     expect(html).toContain('data-hz-topology-toolbar-action-policy="scope-controls-only"');
     expect(html).toContain('data-hz-topology-toolbar-canvas-action-policy="in-canvas-g6-toolbar"');
     expect(html).toContain('data-hz-topology-toolbar-chrome="frameless"');
@@ -6128,6 +6285,7 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-toolbar-control-strip="source-depth-group-reset"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-display="contents"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
     expect(html).toContain('data-hz-topology-control="depth"');
     expect(html).toContain('data-hz-topology-control-depth-owner="hertzbeat-ui-toolbar-depth-control"');
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index 68ccfb32f6..5208a8eb90 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -10492,7 +10492,6 @@ export type HzTopologyLegendItem = {
   pattern?: 'solid' | 'dashed' | 'muted';
   color?: string;
   fill?: string;
-  swatch?: 'line';
   visualSource?: 'hertzbeat-status-token' | 'hertzbeat-interaction-token' | 'hertzbeat-edge-token' | 'lucide-react';
   iconSrc?: string;
   iconAlt?: string;
@@ -10781,6 +10780,7 @@ export type HzTopologyPathSummaryAction = Omit<React.AnchorHTMLAttributes<HTMLAn
 };
 
 export type HzTopologyPathSummaryBoundary = 'none' | 'section' | 'framed' | 'flush';
+export type HzTopologyPathSummaryInteractionState = 'preview' | 'hovered' | 'selected';
 
 export type HzTopologyPathSummaryProps = React.HTMLAttributes<HTMLElement> & {
   title: React.ReactNode;
@@ -10792,6 +10792,7 @@ export type HzTopologyPathSummaryProps = React.HTMLAttributes<HTMLElement> & {
   evidenceBadges?: string[];
   actions?: HzTopologyPathSummaryAction[];
   boundary?: HzTopologyPathSummaryBoundary;
+  interactionState?: HzTopologyPathSummaryInteractionState;
   selectedEdgeId?: string;
   hoveredEdgeId?: string;
   sourceId?: string;
@@ -11450,13 +11451,58 @@ export function HzTopologyDetailDrawer({
 }: HzTopologyDetailDrawerProps) {
   const { className: boundaryClassName, ...boundaryRestProps } = boundaryProps ?? {};
   const graphFirst = density === 'graph-first';
+  const scrollResetKey = `${kind}:${subjectId ?? 'none'}:${sourceId ?? 'none'}:${targetId ?? 'none'}:${relationType ?? 'unknown'}:${sourceKind ?? 'unknown'}:${entityType ?? 'unknown'}`;
+  const drawerRef = React.useRef<HTMLElement | null>(null);
+  const previousScrollResetKeyRef = React.useRef(scrollResetKey);
+
+  React.useEffect(() => {
+    const drawer = drawerRef.current;
+
+    if (!drawer || previousScrollResetKeyRef.current === scrollResetKey) {
+      return;
+    }
+
+    drawer.scrollTop = 0;
+    previousScrollResetKeyRef.current = scrollResetKey;
+  }, [scrollResetKey]);
+
+  const signalActionGroup =
+    signalActions.length > 0 ? (
+      <div
+        className={cn(
+          'grid',
+          graphFirst
+            ? 'sticky top-0 z-10 -mx-2 mt-2 gap-1 border-y border-[#252832] bg-[#0b0c0f] px-2 py-2'
+            : 'mt-3 gap-2'
+        )}
+        data-hz-topology-detail-actions="signals"
+        data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"
+        data-hz-topology-detail-signal-action-placement={graphFirst ? 'header-dock' : 'footer'}
+        data-hz-topology-detail-signal-action-placement-owner="hertzbeat-ui-detail-signal-action-placement"
+        data-hz-topology-detail-signal-action-sticky={graphFirst ? 'top-with-header-context' : 'none'}
+      >
+        {signalActionsLabel ? (
+          <div
+            className="truncate text-[10px] font-semibold uppercase tracking-[0.08em] text-[#727b8c]"
+            data-hz-topology-detail-signal-label="signals"
+            data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"
+          >
+            {signalActionsLabel}
+          </div>
+        ) : null}
+        {signalActions.map(action => (
+          <HzTopologyDetailDrawerActionLink key={action.id} action={action} attributeName="data-hz-topology-detail-signal-action" />
+        ))}
+      </div>
+    ) : null;
 
   return (
     <section
       {...props}
+      ref={drawerRef}
       className={cn(
         'min-w-0',
-        graphFirst ? 'bg-[#0b0c0f] p-2 text-[12px]' : 'bg-[#101217] p-3',
+        graphFirst ? 'hb-scrollbar max-h-[560px] overflow-y-auto overscroll-contain bg-[#0b0c0f] p-2 text-[12px]' : 'bg-[#101217] p-3',
         topologyDetailDrawerSurfaceClassName[surface],
         className
       )}
@@ -11467,6 +11513,13 @@ export function HzTopologyDetailDrawer({
       data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"
       data-hz-topology-detail-visual-weight={graphFirst ? 'low-interruption' : 'balanced'}
       data-hz-topology-detail-visual-weight-owner="hertzbeat-ui-detail-visual-weight"
+      data-hz-topology-detail-rail-fit={graphFirst ? 'compact-side-rail' : 'standard'}
+      data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"
+      data-hz-topology-detail-rail-max-block={graphFirst ? 'bounded-560px' : 'unbounded'}
+      data-hz-topology-detail-overflow-policy={graphFirst ? 'internal-scroll' : 'document-flow'}
+      data-hz-topology-detail-scroll-reset={graphFirst ? 'identity-change' : 'none'}
+      data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"
+      data-hz-topology-detail-scroll-reset-key={scrollResetKey}
       data-hz-topology-detail-surface={surface}
       data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"
       data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"
@@ -11499,6 +11552,7 @@ export function HzTopologyDetailDrawer({
           </div>
         ) : null}
       </div>
+      {graphFirst ? signalActionGroup : null}
       {boundary ? (
         <div
           {...boundaryRestProps}
@@ -11569,26 +11623,7 @@ export function HzTopologyDetailDrawer({
           ))}
         </div>
       ) : null}
-      {signalActions.length > 0 ? (
-        <div
-          className={cn('grid', graphFirst ? 'mt-2 gap-1' : 'mt-3 gap-2')}
-          data-hz-topology-detail-actions="signals"
-          data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"
-        >
-          {signalActionsLabel ? (
-            <div
-              className="truncate text-[10px] font-semibold uppercase tracking-[0.08em] text-[#727b8c]"
-              data-hz-topology-detail-signal-label="signals"
-              data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"
-            >
-              {signalActionsLabel}
-            </div>
-          ) : null}
-          {signalActions.map(action => (
-            <HzTopologyDetailDrawerActionLink key={action.id} action={action} attributeName="data-hz-topology-detail-signal-action" />
-          ))}
-        </div>
-      ) : null}
+      {!graphFirst ? signalActionGroup : null}
     </section>
   );
 }
@@ -12179,6 +12214,7 @@ export function HzTopologyPathSummary({
   evidenceBadges = [],
   actions = [],
   boundary = 'none',
+  interactionState,
   selectedEdgeId,
   hoveredEdgeId,
   sourceId,
@@ -12188,6 +12224,9 @@ export function HzTopologyPathSummary({
   className,
   ...props
 }: HzTopologyPathSummaryProps) {
+  const resolvedInteractionState: HzTopologyPathSummaryInteractionState =
+    interactionState ?? (selectedEdgeId ? 'selected' : hoveredEdgeId ? 'hovered' : 'preview');
+
   return (
     <section
       {...props}
@@ -12202,6 +12241,11 @@ export function HzTopologyPathSummary({
       data-hz-topology-path-summary-boundary={boundary}
       data-hz-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"
       data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"
+      data-topology-path-summary-interaction-state={resolvedInteractionState}
+      data-hz-topology-path-summary-interaction-state={resolvedInteractionState}
+      data-hz-topology-path-summary-interaction-state-owner="hertzbeat-ui-path-summary-interaction-state"
+      data-topology-path-summary-selected-edge-id={selectedEdgeId ?? 'none'}
+      data-topology-path-summary-hovered-edge-id={hoveredEdgeId ?? 'none'}
       data-hz-topology-path-selected-edge={selectedEdgeId ?? 'none'}
       data-hz-topology-path-hovered-edge={hoveredEdgeId ?? 'none'}
       data-hz-topology-path-source-id={sourceId ?? 'none'}
@@ -12662,7 +12706,7 @@ export function HzTopologyWorkbenchGrid({
       {...props}
       className={cn(
         'grid min-h-[760px] bg-[#08090c]',
-        layout === 'canvas-companion' ? 'xl:grid-cols-[minmax(0,1fr)_320px]' : '',
+        layout === 'canvas-companion' ? 'lg:grid-cols-[minmax(0,1fr)_320px]' : '',
         className
       )}
       data-hz-ui="topology-workbench-grid"
@@ -12683,7 +12727,7 @@ const topologyWorkbenchSlotSurfaceClassName: Record<HzTopologyWorkbenchSlotSurfa
 };
 
 const topologyWorkbenchSlotKindClassName: Record<HzTopologyWorkbenchSlotKind, string> = {
-  canvas: 'min-w-0 xl:sticky xl:top-[64px] xl:self-start',
+  canvas: 'min-w-0 lg:sticky lg:top-[64px] lg:self-start',
   companion: 'min-w-0'
 };
 
@@ -12974,8 +13018,9 @@ export function HzTopologyToolbar({
   const hasScopeControls = depthOptions.length > 0 || hasLayoutControl || groupByOptions.length > 0 || Boolean(resetLabel);
   const secondaryVisibility = graphFirst ? 'assistive' : 'visible';
   const controlStripLayout = graphFirst ? 'inline-overflow' : 'stacked-grid';
+  const controlStripDisplay = graphFirst ? 'contents' : 'grid';
   const chrome = graphFirst && boundary === 'none' ? 'frameless' : 'surface';
-  const selectTriggerClassName = graphFirst ? 'h-7 !gap-1.5 !pl-1 !pr-1.5' : 'h-8';
+  const selectTriggerClassName = graphFirst ? 'h-7 !gap-1.5 !px-2' : 'h-8';
 
   return (
     <section
@@ -12984,7 +13029,7 @@ export function HzTopologyToolbar({
         'min-w-0',
         chrome === 'frameless' ? 'bg-transparent' : 'bg-[var(--hz-ui-surface)]',
         graphFirst
-          ? 'grid items-center gap-1.5 overflow-x-auto px-0 py-1 [grid-template-columns:132px_minmax(220px,1fr)_132px_auto]'
+          ? 'grid items-center gap-1.5 overflow-x-auto px-0 py-1 [grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]'
           : 'grid gap-2 px-4 py-2 lg:grid-cols-[180px_minmax(0,1fr)_104px_104px]',
         topologyToolbarBoundaryClassName[boundary],
         className
@@ -13003,6 +13048,8 @@ export function HzTopologyToolbar({
       data-hz-topology-toolbar-select-padding={graphFirst ? 'compact-flush' : 'default'}
       data-hz-topology-toolbar-row-separator={graphFirst ? 'none' : 'soft'}
       data-hz-topology-toolbar-control-gap={graphFirst ? '6px' : '8px'}
+      data-hz-topology-toolbar-control-flow={graphFirst ? 'single-grid-row' : 'stacked-grid'}
+      data-hz-topology-toolbar-empty-offset={graphFirst ? 'none' : undefined}
       data-hz-topology-toolbar-visual-weight={graphFirst ? 'low-interruption' : 'balanced'}
       data-hz-topology-toolbar-visual-weight-owner="hertzbeat-ui-toolbar-visual-weight"
       data-hz-topology-toolbar-secondary-visibility={secondaryVisibility}
@@ -13021,7 +13068,7 @@ export function HzTopologyToolbar({
         options={environmentOptions}
         value={environmentValue}
         onChange={event => onEnvironmentChange?.(event.currentTarget.value)}
-        className={graphFirst ? 'w-[132px] min-w-0' : undefined}
+        className={graphFirst ? 'w-[112px] min-w-0' : undefined}
         triggerClassName={selectTriggerClassName}
       />
       <HzInput
@@ -13044,7 +13091,7 @@ export function HzTopologyToolbar({
           options={sourceKindOptions}
           value={sourceKindValue}
           onChange={event => onSourceKindChange?.(event.currentTarget.value)}
-          className={graphFirst ? 'w-[132px] shrink-0' : undefined}
+          className={graphFirst ? 'w-[148px] shrink-0' : undefined}
           triggerClassName={selectTriggerClassName}
         />
       ) : null}
@@ -13075,13 +13122,14 @@ export function HzTopologyToolbar({
         <div
           className={cn(
             graphFirst
-              ? 'flex min-w-0 shrink-0 items-center justify-end gap-1.5'
+              ? 'contents'
               : 'grid min-w-0 border-t border-[var(--hz-ui-line-faint)] sm:grid-cols-2 lg:col-span-4',
             graphFirst ? '' : 'gap-2 pt-2 xl:grid-cols-[120px_160px_160px_auto]'
           )}
           data-hz-topology-toolbar-control-strip={graphFirst ? 'source-depth-group-reset' : 'depth-layout-group-reset'}
           data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"
           data-hz-topology-toolbar-control-strip-layout={controlStripLayout}
+          data-hz-topology-toolbar-control-strip-display={controlStripDisplay}
           data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"
         >
           {depthOptions.length > 0 ? (
@@ -13238,11 +13286,12 @@ export function HzTopologyToolbar({
 }
 
 export type HzTopologyEmptyStateBoundary = 'default' | 'flush' | 'canvas';
+export type HzTopologyEmptyStateCopyVisibility = 'visible' | 'assistive';
 
 const topologyEmptyStateBoundaryClassNames: Record<HzTopologyEmptyStateBoundary, string> = {
   default: 'border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]',
   flush: 'border-y border-x-0 border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]',
-  canvas: 'border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]'
+  canvas: 'border-0 bg-transparent shadow-none'
 };
 
 export type HzTopologyEmptyStateProps = React.HTMLAttributes<HTMLElement> & {
@@ -13260,6 +13309,7 @@ export type HzTopologyEmptyStateProps = React.HTMLAttributes<HTMLElement> & {
   kind?: 'api-empty' | 'degraded' | 'filtered-empty';
   placement?: 'inline' | 'canvas-center';
   boundary?: HzTopologyEmptyStateBoundary;
+  copyVisibility?: HzTopologyEmptyStateCopyVisibility;
 };
 
 export function HzTopologyEmptyState({
@@ -13277,6 +13327,7 @@ export function HzTopologyEmptyState({
   kind = 'api-empty',
   placement = 'inline',
   boundary = 'default',
+  copyVisibility = 'visible',
   className,
   ...props
 }: HzTopologyEmptyStateProps) {
@@ -13298,6 +13349,7 @@ export function HzTopologyEmptyState({
       data-hz-topology-empty-kind={kind}
       data-hz-topology-empty-boundary={boundary}
       data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"
+      data-hz-topology-empty-boundary-visual={boundary === 'canvas' ? 'frameless-canvas' : 'surface'}
       data-hz-topology-empty-placement={placement}
       data-hz-topology-empty-source={sourceLabel}
       data-hz-topology-empty-time-scope={timeScope}
@@ -13309,11 +13361,17 @@ export function HzTopologyEmptyState({
       data-hz-topology-empty-depth={depth ?? 'unknown'}
       data-hz-topology-empty-result-count={typeof resultCount === 'number' ? resultCount : 'unknown'}
       data-hz-topology-empty-evidence-sources={normalizedEvidenceSources}
+      data-hz-topology-empty-copy-visibility={copyVisibility}
     >
       <div className="text-[13px] font-semibold text-[#f3f6fb]" data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title">
         {title}
       </div>
-      <div className="mt-2 text-[12px] leading-5 text-[#8f99ab]" data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy">
+      <div
+        className={cn(
+          copyVisibility === 'assistive' ? 'sr-only' : 'mt-2 text-[12px] leading-5 text-[#8f99ab]'
+        )}
+        data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"
+      >
         {copy}
       </div>
       {sourceLabel || timeScope ? (
@@ -13474,6 +13532,7 @@ export type HzTopologyCompanionJumpListProps = React.HTMLAttributes<HTMLElement>
   items: HzTopologyCompanionJumpListItem[];
   density?: HzTopologyCompanionJumpListDensity;
   activeMode?: HzTopologyCompanionJumpListActiveMode;
+  activeResetKey?: string | number;
   ariaLabel?: string;
 };
 
@@ -13580,14 +13639,44 @@ export function HzTopologyCompanionJumpList({
   items,
   density = 'compact',
   activeMode = 'manual',
+  activeResetKey,
   ariaLabel = 'Topology companion sections',
   className,
   ...rest
 }: HzTopologyCompanionJumpListProps) {
   const jumpListRef = React.useRef<HTMLElement | null>(null);
   const manualActiveId = React.useMemo(() => items.find(item => item.active)?.id, [items]);
+  const manualActiveHref = items.find(item => item.id === manualActiveId)?.href;
   const [scrollActiveId, setScrollActiveId] = React.useState<string | undefined>();
   const resolvedActiveId = activeMode === 'contained-rail-scroll' ? (scrollActiveId ?? manualActiveId) : manualActiveId;
+  const didRunSelectionSyncRef = React.useRef(false);
+
+  React.useEffect(() => {
+    if (activeMode !== 'contained-rail-scroll') {
+      didRunSelectionSyncRef.current = false;
+      return;
+    }
+    const shouldSyncLocation = didRunSelectionSyncRef.current && activeResetKey !== undefined;
+    didRunSelectionSyncRef.current = true;
+    setScrollActiveId(undefined);
+    if (!manualActiveHref?.startsWith('#') || typeof document === 'undefined') return;
+    const jumpList = jumpListRef.current;
+    const rail = jumpList?.closest<HTMLElement>('[data-hz-ui="topology-companion-rail"][data-hz-topology-companion-scroll="contained"]');
+    const target = document.getElementById(manualActiveHref.slice(1));
+    if (!jumpList || !rail || !target || !rail.contains(target)) return;
+    const railRect = rail.getBoundingClientRect();
+    const targetRect = target.getBoundingClientRect();
+    const jumpListHeight = jumpList.getBoundingClientRect().height;
+    const targetTop = Math.max(0, rail.scrollTop + targetRect.top - railRect.top - jumpListHeight - 8);
+    if (typeof rail.scrollTo === 'function') {
+      rail.scrollTo({ top: targetTop, behavior: 'auto' });
+    } else {
+      rail.scrollTop = targetTop;
+    }
+    if (shouldSyncLocation && typeof window !== 'undefined' && window.history?.replaceState) {
+      window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${manualActiveHref}`);
+    }
+  }, [activeMode, activeResetKey, manualActiveHref, manualActiveId]);
 
   React.useEffect(() => {
     if (activeMode !== 'contained-rail-scroll' || typeof document === 'undefined') return undefined;
@@ -13636,7 +13725,6 @@ export function HzTopologyCompanionJumpList({
       if (nextActiveId) setScrollActiveId(current => (current === nextActiveId ? current : nextActiveId));
     };
 
-    updateActiveSection();
     rail.addEventListener('scroll', updateActiveSection, { passive: true });
     return () => rail.removeEventListener('scroll', updateActiveSection);
   }, [activeMode, items]);
@@ -13663,6 +13751,13 @@ export function HzTopologyCompanionJumpList({
       data-hz-topology-companion-jump-list-scroll-scope-owner="hertzbeat-ui-companion-jump-list-scroll-scope"
       data-hz-topology-companion-jump-list-active-mode={activeMode}
       data-hz-topology-companion-jump-list-active-mode-owner="hertzbeat-ui-companion-jump-list-active-mode"
+      data-hz-topology-companion-jump-list-selection-sync="manual-active-resets-scroll-active"
+      data-hz-topology-companion-jump-list-selection-sync-owner="hertzbeat-ui-companion-jump-list-selection-sync"
+      data-hz-topology-companion-jump-list-selection-scroll="active-section"
+      data-hz-topology-companion-jump-list-selection-scroll-owner="hertzbeat-ui-companion-jump-list-selection-scroll"
+      data-hz-topology-companion-jump-list-selection-url-policy="replace-active-section-hash"
+      data-hz-topology-companion-jump-list-selection-url-policy-owner="hertzbeat-ui-companion-jump-list-selection-url-policy"
+      data-hz-topology-companion-jump-list-active-reset-key={activeResetKey ?? manualActiveId ?? 'none'}
     >
       <div className="flex min-w-max items-center gap-1">
         {items.map(({ id, href, label, active = false, className: itemClassName, onClick, ...itemProps }) => {
@@ -13811,6 +13906,9 @@ export type HzTopologyMetricTableLabels = {
   renderWindowFilterPartial?: React.ReactNode;
   renderWindowFilterHidden?: React.ReactNode;
   renderWindowFilterUnknown?: React.ReactNode;
+  renderWindowEdgeSummary?: React.ReactNode;
+  renderWindowRowSummary?: (rendered: number, total: number) => React.ReactNode;
+  renderWindowShowMore?: (next: number, total: number) => React.ReactNode;
 };
 
 export type HzTopologyMetricTableRenderWindowCompanion = {
@@ -13818,6 +13916,8 @@ export type HzTopologyMetricTableRenderWindowCompanion = {
   totalNodeCount: number;
   renderedNodeCount: number;
   hiddenNodeCount: number;
+  totalEdgeCount?: number;
+  renderedEdgeCount?: number;
   visibleNodeBudget: number;
   tableCompanion?: 'optional' | 'recommended' | 'required';
   priorityNodeIds?: string[];
@@ -13828,6 +13928,7 @@ export type HzTopologyMetricTableBoundary = 'default' | 'framed' | 'flush';
 export type HzTopologyMetricTableDensity = 'compact' | 'graph-first';
 export type HzTopologyMetricRowWindowVisibility = 'visible' | 'partial' | 'hidden' | 'unknown';
 export type HzTopologyMetricTableRenderWindowFilter = 'all' | HzTopologyMetricRowWindowVisibility;
+const HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET = 120;
 
 const topologyMetricTableBoundaryClassName: Record<HzTopologyMetricTableBoundary, string> = {
   default: 'border-y border-[var(--hz-ui-line-soft)]',
@@ -13893,6 +13994,7 @@ export function HzTopologyMetricTable({
   title,
   rows,
   selectedRowId,
+  selectionSource = 'none',
   emptyLabel = 'No topology evidence',
   labels,
   renderWindowCompanion,
@@ -13907,6 +14009,7 @@ export function HzTopologyMetricTable({
   title: React.ReactNode;
   rows: HzTopologyMetricRow[];
   selectedRowId?: string;
+  selectionSource?: string;
   emptyLabel?: React.ReactNode;
   labels?: HzTopologyMetricTableLabels;
   renderWindowCompanion?: HzTopologyMetricTableRenderWindowCompanion;
@@ -13929,12 +14032,29 @@ export function HzTopologyMetricTable({
     : 'none';
   const renderedNodeIds = renderWindowCompanion?.renderedNodeIds;
   const renderedNodeIdSet = renderedNodeIds?.length ? new Set(renderedNodeIds) : undefined;
+  const tableEdgeCount = typeof renderWindowCompanion?.totalEdgeCount === 'number'
+    ? renderWindowCompanion.totalEdgeCount
+    : rows.length;
+  const canvasRenderedEdgeCount = typeof renderWindowCompanion?.renderedEdgeCount === 'number'
+    ? renderWindowCompanion.renderedEdgeCount
+    : tableEdgeCount;
+  const hasCanvasEdgeSummary =
+    renderWindowMode === 'windowed' &&
+    typeof renderWindowCompanion?.totalEdgeCount === 'number' &&
+    typeof renderWindowCompanion?.renderedEdgeCount === 'number';
+  const edgeCountPolicy = hasCanvasEdgeSummary ? 'canvas-rendered-vs-table-total' : 'row-window-visibility';
+  const renderWindowEdgeSummary =
+    labels?.renderWindowEdgeSummary ?? `${canvasRenderedEdgeCount}/${tableEdgeCount} rendered in canvas`;
   const rowsWithWindowVisibility = rows.map(row => {
     const sourceVisible = resolveTopologyMetricEndpointVisibility(row.sourceNodeId, renderWindowMode, renderedNodeIdSet);
     const targetVisible = resolveTopologyMetricEndpointVisibility(row.targetNodeId, renderWindowMode, renderedNodeIdSet);
     const rowWindowVisibility = resolveTopologyMetricRowWindowVisibility(sourceVisible, targetVisible);
     return { row, sourceVisible, targetVisible, rowWindowVisibility };
   });
+  const selectedRowWithWindowVisibility = selectedRowId
+    ? rowsWithWindowVisibility.find(({ row }) => row.id === selectedRowId)
+    : undefined;
+  const hasMetricTableSelection = Boolean(selectedRowId && selectionSource !== 'none');
   const renderWindowRowCounts = rowsWithWindowVisibility.reduce<Record<HzTopologyMetricRowWindowVisibility, number>>(
     (counts, row) => {
       counts[row.rowWindowVisibility] += 1;
@@ -13943,10 +14063,44 @@ export function HzTopologyMetricTable({
     { visible: 0, partial: 0, hidden: 0, unknown: 0 }
   );
   const activeRenderWindowFilter = normalizeTopologyMetricTableRenderWindowFilter(renderWindowFilter);
+  const [rowRenderPage, setRowRenderPage] = React.useState(1);
+  React.useEffect(() => {
+    setRowRenderPage(1);
+  }, [activeRenderWindowFilter, rows.length]);
   const filteredRowsWithWindowVisibility =
     activeRenderWindowFilter === 'all'
       ? rowsWithWindowVisibility
       : rowsWithWindowVisibility.filter(row => row.rowWindowVisibility === activeRenderWindowFilter);
+  const shouldBudgetRenderedRows =
+    renderWindowMode === 'windowed' &&
+    filteredRowsWithWindowVisibility.length > HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET;
+  const rowRenderBudget = shouldBudgetRenderedRows
+    ? Math.min(filteredRowsWithWindowVisibility.length, HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET * rowRenderPage)
+    : filteredRowsWithWindowVisibility.length;
+  const budgetedRowsWithWindowVisibility = shouldBudgetRenderedRows
+    ? filteredRowsWithWindowVisibility.slice(0, rowRenderBudget)
+    : filteredRowsWithWindowVisibility;
+  const selectedFilteredRowWithWindowVisibility = selectedRowId
+    ? filteredRowsWithWindowVisibility.find(({ row }) => row.id === selectedRowId)
+    : undefined;
+  const renderedRowsWithWindowVisibility =
+    shouldBudgetRenderedRows &&
+    selectedFilteredRowWithWindowVisibility &&
+    !budgetedRowsWithWindowVisibility.some(({ row }) => row.id === selectedFilteredRowWithWindowVisibility.row.id)
+      ? [...budgetedRowsWithWindowVisibility, selectedFilteredRowWithWindowVisibility]
+      : budgetedRowsWithWindowVisibility;
+  const hiddenRenderedRowCount = Math.max(0, filteredRowsWithWindowVisibility.length - renderedRowsWithWindowVisibility.length);
+  const nextRowRenderCount = shouldBudgetRenderedRows
+    ? Math.min(filteredRowsWithWindowVisibility.length, rowRenderBudget + HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET)
+    : filteredRowsWithWindowVisibility.length;
+  const canShowMoreRenderedRows = shouldBudgetRenderedRows && rowRenderBudget < filteredRowsWithWindowVisibility.length;
+  const rowRenderPolicy = shouldBudgetRenderedRows ? 'windowed-dom-budget' : 'all-filtered-rows';
+  const renderWindowRowSummary =
+    labels?.renderWindowRowSummary?.(renderedRowsWithWindowVisibility.length, filteredRowsWithWindowVisibility.length) ??
+    `Showing ${renderedRowsWithWindowVisibility.length} of ${filteredRowsWithWindowVisibility.length} rows`;
+  const renderWindowShowMore =
+    labels?.renderWindowShowMore?.(nextRowRenderCount, filteredRowsWithWindowVisibility.length) ??
+    `Show ${nextRowRenderCount} of ${filteredRowsWithWindowVisibility.length} rows`;
   const renderWindowFilterOptions: Array<{
     id: HzTopologyMetricTableRenderWindowFilter;
     label: React.ReactNode;
@@ -13969,6 +14123,7 @@ export function HzTopologyMetricTable({
       )}
       data-hz-ui="topology-metric-table"
       data-hz-topology-primitive="metric-table"
+      data-hz-topology-metric-table-root="true"
       data-hz-topology-metric-table-density={density}
       data-hz-topology-metric-table-density-owner="hertzbeat-ui-metric-table-density"
       data-hz-topology-metric-table-visual-weight={graphFirst ? 'low-interruption' : 'balanced'}
@@ -13977,12 +14132,33 @@ export function HzTopologyMetricTable({
       data-hz-topology-metric-table-boundary={boundary}
       data-hz-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
       data-hz-topology-metric-rows={rows.length}
+      data-hz-topology-metric-table-total-rows={rows.length}
       data-hz-topology-metric-table-interaction="row-select-detail"
+      data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"
+      data-hz-topology-metric-table-selected-edge-id={hasMetricTableSelection ? selectedRowId : 'none'}
+      data-hz-topology-metric-table-selection-source={hasMetricTableSelection ? selectionSource : 'none'}
+      data-hz-topology-metric-table-selected-row-render-window-visibility={
+        hasMetricTableSelection ? selectedRowWithWindowVisibility?.rowWindowVisibility ?? 'unknown' : 'none'
+      }
+      data-hz-topology-metric-table-selected-row-source-visible={
+        hasMetricTableSelection && selectedRowWithWindowVisibility ? String(selectedRowWithWindowVisibility.sourceVisible) : 'unknown'
+      }
+      data-hz-topology-metric-table-selected-row-target-visible={
+        hasMetricTableSelection && selectedRowWithWindowVisibility ? String(selectedRowWithWindowVisibility.targetVisible) : 'unknown'
+      }
+      data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"
+      data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"
+      data-hz-topology-metric-table-filter-url-policy="preserve-current-url"
       data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"
       data-hz-topology-metric-table-render-window-mode={renderWindowMode}
       data-hz-topology-metric-table-render-window-total-node-count={renderWindowCompanion?.totalNodeCount ?? 0}
       data-hz-topology-metric-table-render-window-rendered-node-count={renderWindowCompanion?.renderedNodeCount ?? rows.length}
       data-hz-topology-metric-table-render-window-hidden-node-count={renderWindowCompanion?.hiddenNodeCount ?? 0}
+      data-hz-topology-metric-table-render-window-total-edge-count={tableEdgeCount}
+      data-hz-topology-metric-table-render-window-rendered-edge-count={canvasRenderedEdgeCount}
+      data-hz-topology-metric-table-edge-count-policy={edgeCountPolicy}
+      data-hz-topology-metric-table-canvas-rendered-edge-count={canvasRenderedEdgeCount}
+      data-hz-topology-metric-table-table-edge-count={tableEdgeCount}
       data-hz-topology-metric-table-render-window-visible-node-budget={renderWindowCompanion?.visibleNodeBudget ?? rows.length}
       data-hz-topology-metric-table-render-window-visible-node-count={renderedNodeIds?.length ?? (renderWindowMode === 'direct' ? renderWindowCompanion?.renderedNodeCount ?? 0 : 0)}
       data-hz-topology-metric-table-render-window-table-companion={renderWindowTableCompanion}
@@ -13991,11 +14167,23 @@ export function HzTopologyMetricTable({
       data-hz-topology-metric-table-visible-row-count={renderWindowRowCounts.visible}
       data-hz-topology-metric-table-partial-row-count={renderWindowRowCounts.partial}
       data-hz-topology-metric-table-hidden-row-count={renderWindowRowCounts.hidden}
+      data-hz-topology-metric-table-hidden-row-proof-owner="hertzbeat-ui-metric-table-hidden-row-proof"
+      data-hz-topology-metric-table-hidden-row-proof={renderWindowRowCounts.hidden > 0 ? 'available' : 'none'}
+      data-hz-topology-metric-table-hidden-row-proof-filter="hidden"
+      data-hz-topology-metric-table-hidden-row-proof-count={renderWindowRowCounts.hidden}
       data-hz-topology-metric-table-unknown-row-count={renderWindowRowCounts.unknown}
       data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"
       data-hz-topology-metric-table-render-window-filter={activeRenderWindowFilter}
       data-hz-topology-metric-table-filtered-row-count={filteredRowsWithWindowVisibility.length}
       data-hz-topology-metric-table-filtered-out-row-count={rows.length - filteredRowsWithWindowVisibility.length}
+      data-hz-topology-metric-table-row-render-policy={rowRenderPolicy}
+      data-hz-topology-metric-table-row-render-reset-policy="filter-change-resets-budget-preserve-selected-row"
+      data-hz-topology-metric-table-row-render-budget={rowRenderBudget}
+      data-hz-topology-metric-table-row-render-page={rowRenderPage}
+      data-hz-topology-metric-table-row-render-next-count={nextRowRenderCount}
+      data-hz-topology-metric-table-row-render-can-show-more={canShowMoreRenderedRows ? 'true' : 'false'}
+      data-hz-topology-metric-table-rendered-row-count={renderedRowsWithWindowVisibility.length}
+      data-hz-topology-metric-table-rendered-hidden-row-count={hiddenRenderedRowCount}
     >
       <header
         className={cn(
@@ -14024,6 +14212,18 @@ export function HzTopologyMetricTable({
         )}
         data-hz-topology-metric-table-filter-controls-owner="hertzbeat-ui-metric-table-filter-controls"
       >
+        {hasCanvasEdgeSummary ? (
+          <span
+            className={cn(
+              'inline-flex min-h-7 items-center border border-[var(--hz-ui-line-soft)] bg-[#08090c] font-mono text-[#8f99ab]',
+              graphFirst ? 'px-2 text-[10px]' : 'px-2.5 text-[11px]'
+            )}
+            data-hz-topology-metric-table-edge-summary-owner="hertzbeat-ui-metric-table-edge-summary"
+            data-hz-topology-metric-table-edge-summary-policy={edgeCountPolicy}
+          >
+            {renderWindowEdgeSummary}
+          </span>
+        ) : null}
         {renderWindowFilterOptions.map(option => {
           const active = option.id === activeRenderWindowFilter;
           return (
@@ -14042,7 +14242,11 @@ export function HzTopologyMetricTable({
               data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"
               data-hz-topology-metric-table-filter-control={option.id}
               data-hz-topology-metric-table-filter-active={active ? 'true' : 'false'}
+              data-hz-topology-metric-table-filter-control-active={active ? 'true' : 'false'}
               data-hz-topology-metric-table-filter-count={option.count}
+              data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"
+              data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"
+              data-hz-topology-metric-table-filter-row-render-reset-policy="reset-row-budget-preserve-selection"
             >
               <span>{option.label}</span>
               <span className="text-[#727b8c]">{option.count}</span>
@@ -14055,11 +14259,21 @@ export function HzTopologyMetricTable({
           {emptyLabel}
         </div>
       ) : (
+        <>
         <div className="grid min-w-0 divide-y divide-[var(--hz-ui-line-faint)]">
-          {filteredRowsWithWindowVisibility.map(({ row, sourceVisible, targetVisible, rowWindowVisibility }) => {
+          {renderedRowsWithWindowVisibility.map(({ row, sourceVisible, targetVisible, rowWindowVisibility }) => {
             const tone = row.tone || (row.errorRate && row.errorRate > 0 ? 'warning' : 'neutral');
             const selected = selectedRowId === row.id;
             const rowActionLabel = labels?.rowAction;
+            const rowWindowContextLabel =
+              rowWindowVisibility === 'visible'
+                ? labels?.renderWindowFilterVisible ?? 'Visible'
+                : rowWindowVisibility === 'partial'
+                  ? labels?.renderWindowFilterPartial ?? 'Partial'
+                  : rowWindowVisibility === 'hidden'
+                    ? labels?.renderWindowFilterHidden ?? 'Hidden'
+                    : labels?.renderWindowFilterUnknown ?? 'Unknown';
+            const showRowWindowContext = renderWindowMode === 'windowed' && (selected || rowWindowVisibility !== 'visible');
             return (
               <button
                 key={row.id}
@@ -14100,6 +14314,9 @@ export function HzTopologyMetricTable({
                 data-hz-topology-error-count={row.errorCount}
                 data-hz-topology-latency-p95-ms={row.latencyP95Ms}
                 data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"
+                data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"
+                data-hz-topology-edge-row-selection-mode="table-row-click-drawer"
+                data-hz-topology-edge-row-selection-url-policy="preserve-current-url"
               >
                 <span className="min-w-0" data-hz-topology-metric-table-endpoints-owner="hertzbeat-ui-metric-table-endpoints">
                   <span
@@ -14127,6 +14344,17 @@ export function HzTopologyMetricTable({
                         {row.sourceKind}
                       </span>
                     ) : null}
+                    {showRowWindowContext ? (
+                      <span
+                        className="border-l border-[var(--hz-ui-line-soft)] pl-1.5 font-semibold text-[#dbe4f0]"
+                        data-hz-topology-edge-row-window-context-owner="hertzbeat-ui-metric-table-row-window-context"
+                        data-hz-topology-edge-row-window-context={rowWindowVisibility}
+                        data-hz-topology-edge-row-window-context-source-visible={sourceVisible}
+                        data-hz-topology-edge-row-window-context-target-visible={targetVisible}
+                      >
+                        {rowWindowContextLabel}
+                      </span>
+                    ) : null}
                     {row.evidenceBadges?.map((badge, index) => (
                       <span
                         key={index}
@@ -14184,6 +14412,35 @@ export function HzTopologyMetricTable({
             );
           })}
         </div>
+        {hiddenRenderedRowCount > 0 ? (
+          <div
+            className={cn(
+              'flex min-w-0 flex-wrap items-center justify-between gap-2 border-t border-[var(--hz-ui-line-faint)] font-mono text-[#8f99ab]',
+              graphFirst ? 'px-2 py-2 text-[10px]' : 'px-3 py-2.5 text-[11px]'
+            )}
+            data-hz-topology-metric-table-row-render-summary-owner="hertzbeat-ui-metric-table-row-render-summary"
+          >
+            <span>{renderWindowRowSummary}</span>
+            {canShowMoreRenderedRows ? (
+              <button
+                type="button"
+                className="inline-flex min-h-7 items-center border border-[var(--hz-ui-line-soft)] bg-[#0b0c0f] px-2 font-semibold text-[#dbe4f0] transition-colors hover:border-[var(--hz-ui-line)]"
+                onClick={() => setRowRenderPage(page => page + 1)}
+                data-hz-topology-metric-table-row-render-action-owner="hertzbeat-ui-metric-table-row-render-action"
+                data-hz-topology-metric-table-row-render-action="show-more"
+                data-hz-topology-metric-table-row-render-action-invariants="append-rows-only no-url-change no-g6-remount viewport-preserved selection-preserved"
+                data-hz-topology-metric-table-row-render-action-effect="append-row-budget"
+                data-hz-topology-metric-table-row-render-action-filter-reset-policy="reset-row-budget-on-filter-change"
+                data-hz-topology-metric-table-row-render-action-url-policy="preserve-current-url"
+                data-hz-topology-metric-table-row-render-action-selection-policy="preserve-selected-edge"
+                data-hz-topology-metric-table-row-render-action-next-count={nextRowRenderCount}
+              >
+                {renderWindowShowMore}
+              </button>
+            ) : null}
+          </div>
+        ) : null}
+        </>
       )}
     </section>
   );
diff --git a/web-next/packages/hertzbeat-ui/src/shell.tsx b/web-next/packages/hertzbeat-ui/src/shell.tsx
new file mode 100644
index 0000000000..9552474bbe
--- /dev/null
+++ b/web-next/packages/hertzbeat-ui/src/shell.tsx
@@ -0,0 +1,690 @@
+'use client';
+
+import * as React from 'react';
+import { X } from 'lucide-react';
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs));
+}
+
+const controlFocusClassName =
+  'focus-visible:border-[var(--hz-ui-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--hz-ui-active-soft)]';
+
+export type HzAiChatPreviewMessage = {
+  role: 'user' | 'assistant' | 'system';
+  label: React.ReactNode;
+  content: React.ReactNode;
+};
+
+export type HzAiChatConversationStatus = 'loading' | 'ready' | 'empty' | 'error';
+export type HzAiChatMessageStatus = 'idle' | 'loading' | 'ready' | 'empty' | 'error';
+export type HzAiChatNewConversationStatus = 'idle' | 'creating' | 'error';
+export type HzAiChatDeleteConversationStatus = 'idle' | 'confirming' | 'deleting' | 'error';
+export type HzAiChatSendStatus = 'idle' | 'sending' | 'error';
+export type HzAiChatConfigStatus = 'idle' | 'loading' | 'ready' | 'saving' | 'saved' | 'error';
+export type HzAiChatScheduleStatus = 'idle' | 'loading' | 'ready' | 'empty' | 'saving' | 'error';
+export type HzAiChatScheduleDeleteStatus = 'idle' | 'confirming' | 'deleting' | 'error';
+
+export type HzAiChatProviderOption = {
+  value: string;
+  label: React.ReactNode;
+  defaultBaseUrl?: string;
+  defaultModel?: string;
+};
+
+export type HzAiChatProviderConfigValue = {
+  code: string;
+  apiKey: string;
+  baseUrl: string;
+  model: string;
+};
+
+export type HzAiChatScheduleRow = {
+  id: string | number;
+  sopName: React.ReactNode;
+  cronExpression: string;
+  enabled: boolean;
+};
+
+export type HzAiChatScheduleSkillOption = {
+  value: string;
+  label: React.ReactNode;
+};
+
+export type HzAiChatScheduleDraft = {
+  sopName: string;
+  cronExpression: string;
+  enabled?: boolean;
+};
+
+export type HzAiChatConversationPreview = {
+  id: string | number;
+  title: React.ReactNode;
+  subtitle?: React.ReactNode;
+  active?: boolean;
+};
+
+export type HzAiChatModalSurfaceProps = React.HTMLAttributes<HTMLDivElement> & {
+  title: React.ReactNode;
+  subtitle?: React.ReactNode;
+  conversationsTitle: React.ReactNode;
+  newChatLabel: React.ReactNode;
+  newChatStatus?: HzAiChatNewConversationStatus;
+  deleteLabel?: string;
+  deleteConfirmLabel?: React.ReactNode;
+  deleteCancelLabel?: React.ReactNode;
+  deleteStatus?: HzAiChatDeleteConversationStatus;
+  deleteConversationId?: HzAiChatConversationPreview['id'] | null;
+  welcomeTitle: React.ReactNode;
+  welcomeDescription: React.ReactNode;
+  inputPlaceholder: string;
+  inputReadOnly?: boolean;
+  inputValue?: string;
+  inputHint?: React.ReactNode;
+  closeLabel: string;
+  sendLabel?: string;
+  sendStatus?: HzAiChatSendStatus;
+  streamingLabel?: React.ReactNode;
+  configOpen?: boolean;
+  configTitle?: React.ReactNode;
+  configDescription?: React.ReactNode;
+  configStatus?: HzAiChatConfigStatus;
+  configStatusLabel?: React.ReactNode;
+  configTriggerLabel?: React.ReactNode;
+  configProviderLabel?: React.ReactNode;
+  configProviderHelp?: React.ReactNode;
+  configApiKeyLabel?: React.ReactNode;
+  configApiKeyHelp?: React.ReactNode;
+  configBaseUrlLabel?: React.ReactNode;
+  configBaseUrlHelp?: React.ReactNode;
+  configModelLabel?: React.ReactNode;
+  configModelHelp?: React.ReactNode;
+  configResetLabel?: React.ReactNode;
+  configSaveLabel?: React.ReactNode;
+  configCancelLabel?: React.ReactNode;
+  configProviderOptions?: HzAiChatProviderOption[];
+  configValue?: HzAiChatProviderConfigValue;
+  scheduleOpen?: boolean;
+  scheduleStatus?: HzAiChatScheduleStatus;
+  scheduleStatusLabel?: React.ReactNode;
+  scheduleTriggerLabel?: React.ReactNode;
+  scheduleTitle?: React.ReactNode;
+  scheduleConfiguredTitle?: React.ReactNode;
+  scheduleCreateTitle?: React.ReactNode;
+  scheduleAddTitle?: React.ReactNode;
+  scheduleSkillLabel?: React.ReactNode;
+  scheduleSkillSelectLabel?: React.ReactNode;
+  scheduleSkillPlaceholder?: React.ReactNode;
+  scheduleCronLabel?: React.ReactNode;
+  scheduleCronHelp?: React.ReactNode;
+  scheduleCronCommonLabel?: React.ReactNode;
+  scheduleCronMondayLabel?: React.ReactNode;
+  scheduleStatusColumnLabel?: React.ReactNode;
+  scheduleActionLabel?: React.ReactNode;
+  scheduleEnabledLabel?: React.ReactNode;
+  scheduleDisabledLabel?: React.ReactNode;
+  scheduleEditLabel?: React.ReactNode;
+  scheduleDeleteLabel?: React.ReactNode;
+  scheduleDeleteConfirmLabel?: React.ReactNode;
+  scheduleDeleteCancelLabel?: React.ReactNode;
+  scheduleDeleteStatus?: HzAiChatScheduleDeleteStatus;
+  scheduleDeleteScheduleId?: HzAiChatScheduleRow['id'] | null;
+  scheduleSaveLabel?: React.ReactNode;
+  scheduleCancelLabel?: React.ReactNode;
+  scheduleCreateLabel?: React.ReactNode;
+  scheduleRows?: HzAiChatScheduleRow[];
+  scheduleSkills?: HzAiChatScheduleSkillOption[];
+  scheduleDraft?: HzAiChatScheduleDraft;
+  scheduleEditDraft?: (HzAiChatScheduleDraft & { id: string | number }) | null;
+  initialMessageLabel?: React.ReactNode;
+  initialMessage?: string;
+  previewMessages?: HzAiChatPreviewMessage[];
+  conversationMessages?: HzAiChatPreviewMessage[];
+  messageStatus?: HzAiChatMessageStatus;
+  messageStatusLabel?: React.ReactNode;
+  conversations?: HzAiChatConversationPreview[];
+  conversationStatus?: HzAiChatConversationStatus;
+  conversationStatusLabel?: React.ReactNode;
+  onNewConversation?: () => void;
+  onConversationSelect?: (id: HzAiChatConversationPreview['id']) => void;
+  onConversationDeleteRequest?: (id: HzAiChatConversationPreview['id']) => void;
+  onConversationDeleteCancel?: () => void;
+  onConversationDeleteConfirm?: (id: HzAiChatConversationPreview['id']) => void;
+  onInputChange?: (value: string) => void;
+  onSendMessage?: () => void;
+  onConfigOpen?: () => void;
+  onConfigClose?: () => void;
+  onConfigSave?: () => void;
+  onConfigResetDefaults?: () => void;
+  onConfigChange?: (value: HzAiChatProviderConfigValue) => void;
+  onScheduleOpen?: () => void;
+  onScheduleClose?: () => void;
+  onScheduleDraftChange?: (value: HzAiChatScheduleDraft) => void;
+  onScheduleCreate?: () => void;
+  onScheduleToggle?: (id: HzAiChatScheduleRow['id'], enabled: boolean) => void;
+  onScheduleEditStart?: (row: HzAiChatScheduleRow) => void;
+  onScheduleEditCancel?: () => void;
+  onScheduleEditChange?: (value: HzAiChatScheduleDraft & { id: string | number }) => void;
+  onScheduleUpdate?: () => void;
+  onScheduleDeleteRequest?: (id: HzAiChatScheduleRow['id']) => void;
+  onScheduleDeleteCancel?: () => void;
+  onScheduleDeleteConfirm?: () => void;
+  onClose?: () => void;
+};
+
+export const HzAiChatModalSurface = React.forwardRef<HTMLDivElement, HzAiChatModalSurfaceProps>(
+  (
+    {
+      className,
+      title,
+      subtitle,
+      conversationsTitle,
+      newChatLabel,
+      welcomeTitle,
+      welcomeDescription,
+      inputPlaceholder,
... 8847 lines suppressed ...


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