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

imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git

commit 238c402e4d0a4118123cf91065d2d4b3e577908c
Author: dark <[email protected]>
AuthorDate: Sun Aug 30 11:46:39 2026 +0800

    fix(hubble): complete final closeout
    
    - unify responsive operations, GraphSpace, and node-detail states
    - preserve independent Server, PD, and Store health ownership
    - close account, session, capability, and compatibility gaps
    - refresh final documentation and cluster screenshot
---
 .../docs/images/showcase/operations-overview.jpg   | Bin 152506 -> 149091 bytes
 .../hugegraph/controller/ConfigController.java     |   5 +-
 .../hugegraph/controller/auth/LoginController.java |   3 +
 .../hugegraph/service/auth/AuthContextService.java |  33 +++-
 .../apache/hugegraph/service/auth/UserService.java |  11 +-
 .../service/op/DefaultOperationsDataService.java   |   4 +-
 .../service/op/LiveOperationsCollector.java        |  24 ++-
 .../service/auth/AuthContextServiceTest.java       |  32 +++-
 .../op/DefaultOperationsDataServiceTest.java       |  28 +++
 .../service/op/LiveOperationsCollectorTest.java    |  46 ++++-
 .../apache/hugegraph/unit/AuthSecurityTest.java    |   4 +
 .../hugegraph/unit/ConfigControllerTest.java       |   6 +-
 .../unit/UserServiceCompatibilityTest.java         |  25 +--
 .../src/i18n/resources/en-US/modules/pages.json    |  23 ++-
 .../src/i18n/resources/zh-CN/modules/pages.json    |  21 +-
 .../hubble-fe/src/pages/Account/EditLayer.js       |  12 +-
 .../hubble-fe/src/pages/Account/SpaceAccess.js     |  41 ++--
 .../src/pages/Account/SpaceAccess.test.js          | 110 ++++++++++-
 .../pages/Account/account-edit-recovery.test.js    |  50 +++--
 .../hubble-fe/src/pages/GraphSpace/Card.js         |  11 +-
 .../hubble-fe/src/pages/GraphSpace/Card.test.js    |  24 +++
 .../hubble-fe/src/pages/GraphSpace/index.js        |   4 +-
 .../src/pages/GraphSpace/index.module.scss         |  43 +++++
 .../hubble-fe/src/pages/Operations/NodeDetail.js   | 211 +++++++++++----------
 .../src/pages/Operations/NodeDetail.test.js        | 171 ++++++++++++++---
 .../hubble-fe/src/pages/Operations/Overview.js     |  42 +++-
 .../src/pages/Operations/Overview.test.js          |  43 ++++-
 .../hubble-fe/src/pages/Operations/components.js   |  28 ++-
 .../src/pages/Operations/components.test.js        |  26 +++
 .../hubble-fe/src/pages/Operations/operations.scss |  36 +++-
 30 files changed, 852 insertions(+), 265 deletions(-)

diff --git a/hugegraph-hubble/docs/images/showcase/operations-overview.jpg 
b/hugegraph-hubble/docs/images/showcase/operations-overview.jpg
index 3c6ad722a..6501a11e8 100644
Binary files a/hugegraph-hubble/docs/images/showcase/operations-overview.jpg 
and b/hugegraph-hubble/docs/images/showcase/operations-overview.jpg differ
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
index 2793fbb6a..18cab88de 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
@@ -70,8 +70,9 @@ public class ConfigController {
                 capabilities.put("auth_enabled",
                                  this.authModeService.update(
                                  client.isServerAuthEnabled()));
-                capabilities.put("graph_create_enabled", true);
-                capabilities.put("cypher_enabled", true);
+                capabilities.put("graph_create_enabled",
+                                 client.supportsGraphCreate());
+                capabilities.put("cypher_enabled", client.supportsCypher());
                 capabilities.put("server_capabilities_verified", true);
                 return capabilities;
             } catch (RuntimeException ignored) {
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
index 3cdcf81be..e5dcc97f8 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
@@ -120,6 +120,9 @@ public class LoginController extends BaseController {
                 this.setSession(Constant.PASSWORD_EXPIRE_AT_KEY,
                                 System.currentTimeMillis() +
                                 TOKEN_EXPIRE_SECONDS * 1000L);
+            } else {
+                this.delSession(Constant.PASSWORD_KEY);
+                this.delSession(Constant.PASSWORD_EXPIRE_AT_KEY);
             }
             return user;
         } catch (Throwable e) {
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
index cc2d45589..3886fc9e5 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
@@ -42,6 +42,7 @@ import org.apache.hugegraph.exception.ForbiddenException;
 import org.apache.hugegraph.exception.InternalException;
 import org.apache.hugegraph.options.HubbleOptions;
 import org.apache.hugegraph.service.op.OperationsCapabilityService;
+import org.apache.hugegraph.service.space.GraphSpaceService;
 
 @Service
 public class AuthContextService {
@@ -85,13 +86,16 @@ public class AuthContextService {
     private final HugeConfig config;
     private final UserService users;
     private final AuthModeService authMode;
+    private final GraphSpaceService graphSpaces;
 
     @Autowired
     public AuthContextService(HugeConfig config, UserService users,
-                              AuthModeService authMode) {
+                              AuthModeService authMode,
+                              GraphSpaceService graphSpaces) {
         this.config = config;
         this.users = users;
         this.authMode = authMode;
+        this.graphSpaces = graphSpaces;
     }
 
     public Map<String, Object> context(HugeClient client, String username) {
@@ -123,8 +127,10 @@ public class AuthContextService {
         boolean profileUpdate =
                 client.supportsPersonalProfileUpdate();
         List<String> writeGraphSpaces = pdEnabled ?
-                                        writeGraphSpaces(user,
-                                                         permissionPresets) :
+                                        writeGraphSpaces(
+                                                user, permissionPresets,
+                                                this.graphSpaces
+                                                    .listAnonymous(client)) :
                                         Collections.emptyList();
         Set<String> capabilities = this.capabilities(pdEnabled, role,
                                                      permissionPresets);
@@ -156,11 +162,15 @@ public class AuthContextService {
             !client.supportsDefaultRole()) {
             return;
         }
+        if (!this.graphSpaces.isAuth(client, graphSpace)) {
+            return;
+        }
 
         UserEntity user = this.users.getpersonal(client, username);
         if (user.isSuperadmin() ||
             contains(user.getAdminSpaces(), graphSpace) ||
-            writeGraphSpaces(user, true).contains(graphSpace)) {
+            writeGraphSpaces(user, true,
+                             Collections.emptyList()).contains(graphSpace)) {
             return;
         }
         throw new ForbiddenException(
@@ -265,15 +275,22 @@ public class AuthContextService {
     }
 
     private static List<String> writeGraphSpaces(UserEntity user,
-                                                 boolean permissionPresets) {
+                                                 boolean permissionPresets,
+                                                 Collection<String> 
publicSpaces) {
         if (user.isSuperadmin()) {
             return Collections.emptyList();
         }
-        if (!permissionPresets) {
-            return sorted(user.getResSpaces());
-        }
 
         Set<String> values = new TreeSet<>();
+        if (publicSpaces != null) {
+            values.addAll(publicSpaces);
+        }
+        if (!permissionPresets) {
+            if (user.getResSpaces() != null) {
+                values.addAll(user.getResSpaces());
+            }
+            return Collections.unmodifiableList(new ArrayList<>(values));
+        }
         if (user.getGraphspacePermissions() != null) {
             for (Map<String, String> permission :
                  user.getGraphspacePermissions()) {
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
index 18676daff..d1c448439 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
@@ -631,17 +631,16 @@ public class UserService extends AuthService {
         String username = previous.name();
         boolean previousSuperAdmin = 
client.auth().listSuperAdmin().contains(username);
         try {
-            if (previousSuperAdmin && !userEntity.isSuperadmin()) {
-                client.auth().delSuperAdmin(username);
-            }
-            if (!previousSuperAdmin && userEntity.isSuperadmin()) {
-                client.auth().addSuperAdmin(username);
-            }
             client.auth().updateUser(user);
             this.graphSpaceUserService.applyPermissionPresets(
                     client, userEntity.getName(),
                     userEntity.getGraphspacePermissions(),
                     userEntity.getPermissionPreset());
+            if (!previousSuperAdmin && userEntity.isSuperadmin()) {
+                client.auth().addSuperAdmin(username);
+            } else if (previousSuperAdmin && !userEntity.isSuperadmin()) {
+                client.auth().delSuperAdmin(username);
+            }
         } catch (RuntimeException error) {
             this.restoreAccountProfile(client, previous, error);
             this.restoreSuperAdmin(client, username, previousSuperAdmin, 
error);
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
index 180394eb0..4818abc8c 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
@@ -278,8 +278,10 @@ public class DefaultOperationsDataService implements 
OperationsDataService {
         }
         String status = "DOWN".equals(current.getStatus()) ? "DOWN" :
                         "DEGRADED";
+        String reason = current.getReason() != null ? current.getReason() :
+                        "partial_refresh_failed";
         return new Snapshot(status, current.getObservedAt(), true,
-                            "partial_refresh_failed", sources, nodes, facts);
+                            reason, sources, nodes, facts);
     }
 
     private void mergeFailedFacts(Map<String, Long> current,
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
index a633f9fe7..b2ccb7503 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
@@ -198,13 +198,19 @@ public class LiveOperationsCollector implements 
OperationsCollector {
         List<Node> nodes = new ArrayList<>();
         Map<String, Long> facts = new LinkedHashMap<>();
         this.collectServer(client, includeMetrics, now, sources, nodes);
+        String clusterReason = null;
         if (this.pdEnabled) {
-            this.collectPd(includeMetrics, now, sources, nodes, facts);
+            clusterReason = this.collectPd(includeMetrics, now, sources, nodes,
+                                           facts);
         } else {
             sources.put("pd", unsupported());
             sources.put("stores", unsupported());
         }
-        return new Snapshot(this.overallStatus(sources), now, false, null,
+        String status = this.overallStatus(sources);
+        if (clusterReason != null && "UP".equals(status)) {
+            status = "DEGRADED";
+        }
+        return new Snapshot(status, now, false, clusterReason,
                             sources, nodes, facts);
     }
 
@@ -220,6 +226,11 @@ public class LiveOperationsCollector implements 
OperationsCollector {
             return;
         }
         if (urls.isEmpty()) {
+            if (this.pdEnabled && this.serverClients != null) {
+                sources.put("server", unavailable(
+                            "topology_fields_unavailable", now));
+                return;
+            }
             this.collectSingleServer(client, this.serverIdentity,
                                      "HugeGraph Server", includeMetrics, now,
                                      sources, nodes);
@@ -409,11 +420,12 @@ public class LiveOperationsCollector implements 
OperationsCollector {
                stableId("server", url).substring("server-".length());
     }
 
-    private void collectPd(boolean includeMetrics, long now,
-                           Map<String, SourceStatus> sources,
-                           List<Node> nodes, Map<String, Long> facts) {
+    private String collectPd(boolean includeMetrics, long now,
+                             Map<String, SourceStatus> sources,
+                             List<Node> nodes, Map<String, Long> facts) {
         String cluster = null;
         String stores = null;
+        String clusterReason = null;
         SourceStatus pdStatus;
         SourceStatus storesStatus;
         try {
@@ -437,6 +449,7 @@ public class LiveOperationsCollector implements 
OperationsCollector {
                                                               now);
                 this.mergeNodes(nodes, topology.getNodes());
                 facts.putAll(topology.getFacts());
+                clusterReason = topology.getReason();
                 pdStatus = available(this.nodeStatus(topology.getNodes(), 
"PD"),
                                      now);
                 clusterParsed = true;
@@ -473,6 +486,7 @@ public class LiveOperationsCollector implements 
OperationsCollector {
         }
         sources.put("pd", pdStatus);
         sources.put("stores", storesStatus);
+        return clusterReason;
     }
 
     private String nodeStatus(List<Node> nodes, String type) {
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
index a697a84bb..30a76fec5 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
@@ -36,6 +36,7 @@ import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.entity.auth.UserEntity;
 import org.apache.hugegraph.exception.ForbiddenException;
 import org.apache.hugegraph.options.HubbleOptions;
+import org.apache.hugegraph.service.space.GraphSpaceService;
 import org.apache.hugegraph.testutil.Assert;
 
 public class AuthContextServiceTest {
@@ -189,6 +190,27 @@ public class AuthContextServiceTest {
                       fixture.client, "alice", "space-a"));
     }
 
+    @Test
+    public void testPdUserRetainsWriteAccessToPublicGraphSpace() {
+        Fixture fixture = new Fixture(true);
+        UserEntity user = user(false, Collections.emptyList());
+        Mockito.when(fixture.client.supportsDefaultRole()).thenReturn(true);
+        Mockito.when(fixture.users.getpersonal(fixture.client, "alice"))
+               .thenReturn(user);
+        Mockito.when(fixture.graphSpaces.listAnonymous(fixture.client))
+               .thenReturn(Collections.singletonList("public"));
+        Mockito.when(fixture.graphSpaces.isAuth(fixture.client, "public"))
+               .thenReturn(false);
+
+        Map<String, Object> context = fixture.service.context(fixture.client,
+                                                              "alice");
+
+        Assert.assertEquals(Collections.singletonList("public"),
+                            scopes(context).get("write_graphspaces"));
+        fixture.service.requireGraphSpaceWrite(fixture.client, "alice",
+                                               "public");
+    }
+
     @Test
     public void testPdReadWriteUserCanWriteGraphSpaceResources() {
         Fixture fixture = new Fixture(true);
@@ -342,6 +364,8 @@ public class AuthContextServiceTest {
         private final UserService users = Mockito.mock(UserService.class);
         private final AuthModeService authMode =
                 Mockito.mock(AuthModeService.class);
+        private final GraphSpaceService graphSpaces =
+                Mockito.mock(GraphSpaceService.class);
         private final AuthContextService service;
 
         private Fixture(boolean pdEnabled) {
@@ -349,8 +373,14 @@ public class AuthContextServiceTest {
                    .thenReturn(pdEnabled);
             Mockito.when(this.client.supportsPersonalProfileUpdate())
                    .thenReturn(true);
+            Mockito.when(this.graphSpaces.listAnonymous(this.client))
+                   .thenReturn(Collections.emptyList());
+            Mockito.when(this.graphSpaces.isAuth(
+                         Mockito.eq(this.client), Mockito.anyString()))
+                   .thenReturn(true);
             this.service = new AuthContextService(this.config, this.users,
-                                                  this.authMode);
+                                                  this.authMode,
+                                                  this.graphSpaces);
         }
     }
 }
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
index f20cbf613..a53aad512 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
@@ -248,6 +248,34 @@ public class DefaultOperationsDataServiceTest {
         Assert.assertTrue(backend.isStale());
     }
 
+    @Test
+    public void testPartialRefreshPreservesCurrentClusterReason() {
+        AtomicInteger calls = new AtomicInteger();
+        OperationsCollector collector = (client, metrics) -> {
+            if (calls.getAndIncrement() == 0) {
+                return fullSnapshot();
+            }
+            Snapshot partial = partialSnapshot();
+            return new Snapshot(partial.getStatus(), partial.getObservedAt(),
+                                false, "cluster_not_ready",
+                                partial.getSources(), partial.getNodes(),
+                                partial.getFacts());
+        };
+        DefaultOperationsDataService service = new 
DefaultOperationsDataService(
+                collector, 5, CLOCK);
+        HugeClient client = client("token-a");
+        Set<String> capabilities = Set.of(
+                OperationsCapabilityService.HEALTH_READ,
+                OperationsCapabilityService.TOPOLOGY_READ);
+
+        service.overview(client, capabilities, false);
+        Map<String, Object> result = service.overview(client, capabilities,
+                                                       true);
+
+        Assert.assertEquals("cluster_not_ready", result.get("reason"));
+        Assert.assertEquals(true, result.get("stale"));
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     public void testPdFailureKeepsFreshStoreCapacityFacts() {
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
index b075a8507..c4e8347d1 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
@@ -122,6 +122,43 @@ public class LiveOperationsCollectorTest {
                                     .getAvailability());
     }
 
+    @Test
+    public void testPdModeDoesNotInventServerWhenDiscoveryIsEmpty()
+           throws IOException {
+        HttpServer pd = pdServer(200, cluster(), 200, stores());
+        LiveOperationsCollector.ServerClientProvider servers =
+                new LiveOperationsCollector.ServerClientProvider() {
+            @Override
+            public java.util.List<String> urls() {
+                return Collections.emptyList();
+            }
+
+            @Override
+            public HugeClient create(String url, String authContext,
+                                     int timeout) {
+                throw new AssertionError("Server probe must not start");
+            }
+        };
+        LiveOperationsCollector collector = collector(true, pd, servers);
+
+        Snapshot snapshot;
+        try {
+            snapshot = collector.collect(serverClient(), false);
+        } finally {
+            collector.close();
+            pd.stop(0);
+        }
+
+        Assert.assertFalse(snapshot.getNodes().stream()
+                                   .anyMatch(node -> "SERVER".equals(
+                                             node.getType())));
+        Assert.assertEquals("UNAVAILABLE",
+                            snapshot.getSources().get("server")
+                                    .getAvailability());
+        Assert.assertEquals("topology_fields_unavailable",
+                            snapshot.getSources().get("server").getReason());
+    }
+
     @Test
     public void testPdServerDiscoveryUsesOperationsDeadline()
            throws IOException {
@@ -257,7 +294,8 @@ public class LiveOperationsCollectorTest {
             pd.stop(0);
         }
 
-        Assert.assertEquals("UP", snapshot.getStatus());
+        Assert.assertEquals("DEGRADED", snapshot.getStatus());
+        Assert.assertEquals("cluster_warn", snapshot.getReason());
         Assert.assertEquals("UP", snapshot.getSources().get("pd").getStatus());
     }
 
@@ -278,7 +316,8 @@ public class LiveOperationsCollectorTest {
         Assert.assertTrue(pdSource.isFresh());
         Assert.assertEquals("UP", pdSource.getStatus());
         Assert.assertNull(pdSource.getReason());
-        Assert.assertEquals("UP", snapshot.getStatus());
+        Assert.assertEquals("DEGRADED", snapshot.getStatus());
+        Assert.assertEquals("cluster_not_ready", snapshot.getReason());
         long pdCount = snapshot.getNodes().stream()
                                .filter(node -> "PD".equals(node.getType()))
                                .count();
@@ -382,7 +421,8 @@ public class LiveOperationsCollectorTest {
             pd.stop(0);
         }
 
-        Assert.assertEquals("UP", snapshot.getStatus());
+        Assert.assertEquals("DEGRADED", snapshot.getStatus());
+        Assert.assertEquals("cluster_state_unknown", snapshot.getReason());
         Assert.assertEquals("UP", snapshot.getSources().get("pd").getStatus());
     }
 
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
index da4cd1206..6f8c8e35b 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
@@ -850,6 +850,10 @@ public class AuthSecurityTest {
     @Test
     public void testModernLoginDoesNotRetainPassword() throws Exception {
         MockHttpServletRequest request = new MockHttpServletRequest();
+        request.getSession().setAttribute(Constant.PASSWORD_KEY,
+                                          "old-legacy-password");
+        request.getSession().setAttribute(Constant.PASSWORD_EXPIRE_AT_KEY,
+                                          Long.MAX_VALUE);
         RequestContextHolder.setRequestAttributes(
                 new ServletRequestAttributes(request));
         TestLoginController controller = new TestLoginController();
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java
index 3e0e235ad..32d45eb43 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java
@@ -35,12 +35,14 @@ import org.apache.hugegraph.service.auth.AuthModeService;
 public class ConfigControllerTest {
 
     @Test
-    public void testPdConfigMarksSuccessfulAuthProbeVerified() {
+    public void testPdConfigUsesDiscoveredServerCapabilities() {
         HugeConfig config = Mockito.mock(HugeConfig.class);
         Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
         HugeClient client = Mockito.mock(HugeClient.class);
         AuthModeService authMode = Mockito.mock(AuthModeService.class);
         Mockito.when(client.isServerAuthEnabled()).thenReturn(false);
+        Mockito.when(client.supportsGraphCreate()).thenReturn(false);
+        Mockito.when(client.supportsCypher()).thenReturn(true);
         Mockito.when(authMode.update(false)).thenReturn(false);
 
         ConfigController controller = new ConfigController() {
@@ -59,7 +61,7 @@ public class ConfigControllerTest {
         Assert.assertEquals(Map.of("pd_enabled", true,
                                    "server_capabilities_verified", true,
                                    "auth_enabled", false,
-                                   "graph_create_enabled", true,
+                                   "graph_create_enabled", false,
                                    "cypher_enabled", true), result);
         Mockito.verify(client).close();
     }
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
index 600fe3728..aa7b78756 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
@@ -430,13 +430,14 @@ public class UserServiceCompatibilityTest {
         account.setGraphspacePermissions(Collections.singletonList(
                 permission("team", "GS_READ_ONLY")));
 
+        ParameterizedException error = null;
         try {
             this.service.update(this.client, account);
-            Assert.fail("Expected combined update to be rejected");
-        } catch (ParameterizedException ignored) {
-            // Expected
+        } catch (ParameterizedException e) {
+            error = e;
         }
 
+        Assert.assertNotNull(error);
         Mockito.verify(this.auth, Mockito.never())
                .updateUser(Mockito.any(User.class));
         Mockito.verify(this.graphSpaceUsers, Mockito.never())
@@ -445,7 +446,7 @@ public class UserServiceCompatibilityTest {
     }
 
     @Test
-    public void testModernUserUpdateRollsBackProfileAndSuperAdmin() {
+    public void testModernUserUpdateKeepsSuperAdminWhenPresetFails() {
         
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
         Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
         User previous = user("alice");
@@ -454,7 +455,7 @@ public class UserServiceCompatibilityTest {
         Mockito.when(this.auth.getUser("u-1")).thenReturn(previous);
         Mockito.when(this.auth.listSuperAdmin())
                .thenReturn(Collections.singletonList("alice"))
-               .thenReturn(Collections.emptyList());
+               .thenReturn(Collections.singletonList("alice"));
         UserEntity account = UserEntity.builder()
                                        .id("u-1")
                                        .name("alice")
@@ -477,8 +478,8 @@ public class UserServiceCompatibilityTest {
         }
 
         Assert.assertSame(failure, error);
-        Mockito.verify(this.auth).delSuperAdmin("alice");
-        Mockito.verify(this.auth).addSuperAdmin("alice");
+        Mockito.verify(this.auth, Mockito.never()).delSuperAdmin("alice");
+        Mockito.verify(this.auth, Mockito.never()).addSuperAdmin("alice");
         ArgumentCaptor<User> updates = ArgumentCaptor.forClass(User.class);
         Mockito.verify(this.auth, Mockito.times(2))
                .updateUser(updates.capture());
@@ -802,13 +803,15 @@ public class UserServiceCompatibilityTest {
 
         this.service.update(this.client, account);
 
-        Mockito.verify(this.auth).delSuperAdmin("alice");
         Mockito.verify(this.graphSpaceUsers, Mockito.never())
                .validatePermissionPresets(Mockito.any(), Mockito.any(),
                                           Mockito.any());
-        Mockito.verify(this.graphSpaceUsers).applyPermissionPresets(
-                this.client, "alice", Collections.emptyList(),
-                "GS_READ_ONLY");
+        org.mockito.InOrder order = Mockito.inOrder(this.auth,
+                                                    this.graphSpaceUsers);
+        order.verify(this.auth).updateUser(Mockito.any(User.class));
+        order.verify(this.graphSpaceUsers).applyPermissionPresets(
+              this.client, "alice", Collections.emptyList(), "GS_READ_ONLY");
+        order.verify(this.auth).delSuperAdmin("alice");
     }
 
     @Test
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
index bee87bfe0..7b64c4d4c 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
@@ -468,6 +468,8 @@
         "existing_account": "Existing Account ID",
         "account_not_found": "This account does not exist. Create it to 
continue assigning access.",
         "account_not_found_contact_admin": "This account does not exist. Ask a 
super administrator to create the user before assigning access.",
+        "missing_response_subject": "Account",
+        "missing_response_predicate": "does not exist",
         "create_account": "Create this account",
         "account_check_failed": "Could not verify this account. Check the 
connection and retry.",
         "batch_failed": "Assigned {{success}} of {{total}} GraphSpaces. 
Failed: {{spaces}}. Check these spaces and retry.",
@@ -1286,6 +1288,7 @@
     "status_degraded": "Attention",
     "status_degraded_help": "Some sources or metrics are unhealthy while core 
services may still be available; review the source details below",
     "unavailable": "Unavailable",
+    "metric_no_data": "No metric data",
     "unsupported": "Unsupported by this service version",
     "metric_malformed": "Malformed metric response",
     "fresh": "Fresh",
@@ -1295,12 +1298,12 @@
     "topology_view": "Topology",
     "node_list_view": "Node list",
     "cluster_nodes": "Cluster nodes",
-    "logical_relationship": "Logical service relationship, not network 
traffic",
     "topology_label": "HugeGraph cluster topology",
     "attention_nodes": "Nodes needing attention",
     "attention_items": "Items needing attention",
     "all_nodes_healthy": "All discovered nodes are healthy",
     "nodes_healthy_source_attention": "All nodes are healthy; the source or 
cluster state above is a separate signal.",
+    "cluster_attention": "Cluster state needs attention",
     "source_attention": "{{source}} source needs attention",
     "view_source_nodes": "View {{source}} nodes",
     "view_nodes": "View nodes",
@@ -1347,6 +1350,8 @@
     "metric_drive": "Drive",
     "metric_raft": "Raft",
     "metric_backend": "Backend",
+    "metric_store_backend": "Storage & partitions",
+    "metric_server_backend": "Graph service",
     "capacity_usage": "Capacity usage",
     "memory_usage": "Memory usage",
     "heap_usage": "Heap memory usage",
@@ -1391,7 +1396,7 @@
       "processors": "Processors",
       "uptime": "Uptime",
       "uptime_seconds": "Uptime",
-      "systemload_average": "System load average",
+      "systemload_average": "1-minute system load",
       "heap": "Heap memory",
       "nonheap": "Non-heap memory",
       "used": "Used",
@@ -1408,15 +1413,17 @@
       "threads_live": "Live threads",
       "heap_used_bytes": "Heap used",
       "nonheap_used_bytes": "Non-heap used",
-      "graphs": "Graphs",
-      "nodes": "Nodes",
-      "backend_counts": "Backend counts",
+      "garbage_collection_count": "GC count",
+      "garbage_collection_time": "GC time",
+      "graphs": "Graph count",
+      "nodes": "Backend node count",
+      "backend_counts": "Storage backends",
       "capacity_bytes": "Total capacity",
       "available_bytes": "Available capacity",
-      "partitions": "Partitions",
-      "leaders": "Leaders",
+      "partitions": "Partition count",
+      "leaders": "Leader partitions",
       "total_space": "Total space",
-      "usable_space": "Usable space",
+      "usable_space": "Available space",
       "free_space": "Free space",
       "size_unit": "Space unit",
       "groups": "Raft groups",
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
index 855b6adfd..987498151 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
@@ -468,6 +468,8 @@
         "existing_account": "已有账号 ID",
         "account_not_found": "账号不存在,可创建此账号后继续分配权限。",
         "account_not_found_contact_admin": "账号不存在,请联系超级管理员创建新用户后再分配权限。",
+        "missing_response_subject": "账号",
+        "missing_response_predicate": "不存在",
         "create_account": "创建此账号",
         "account_check_failed": "无法确认账号是否存在,请检查连接后重试。",
         "batch_failed": "已成功分配 {{success}}/{{total}} 个 
GraphSpace;以下空间失败:{{spaces}}。请检查后重试。",
@@ -1286,6 +1288,7 @@
     "status_degraded": "需关注",
     "status_degraded_help": "部分来源或指标异常,核心服务可能仍可用;请查看下方来源原因",
     "unavailable": "不可用",
+    "metric_no_data": "暂无指标数据",
     "unsupported": "当前服务版本不支持",
     "metric_malformed": "指标响应格式异常",
     "fresh": "数据新鲜",
@@ -1295,12 +1298,12 @@
     "topology_view": "拓扑图",
     "node_list_view": "节点列表",
     "cluster_nodes": "集群节点",
-    "logical_relationship": "表示逻辑服务关系,不表示网络流量",
     "topology_label": "HugeGraph 集群拓扑",
     "attention_nodes": "需关注的节点",
     "attention_items": "需关注项",
     "all_nodes_healthy": "已发现的节点均健康",
     "nodes_healthy_source_attention": "节点均健康;上方是独立的来源或集群状态提醒。",
+    "cluster_attention": "集群状态需关注",
     "source_attention": "{{source}} 来源需关注",
     "view_source_nodes": "查看 {{source}} 节点",
     "view_nodes": "查看节点",
@@ -1345,8 +1348,10 @@
     "back_to_nodes": "返回节点列表",
     "metric_system": "系统",
     "metric_drive": "磁盘",
-    "metric_raft": "Raft 共识",
+    "metric_raft": "Raft",
     "metric_backend": "后端",
+    "metric_store_backend": "存储与分区",
+    "metric_server_backend": "图服务",
     "capacity_usage": "容量使用",
     "memory_usage": "内存使用",
     "heap_usage": "堆内存使用",
@@ -1391,7 +1396,7 @@
       "processors": "处理器数",
       "uptime": "运行时间",
       "uptime_seconds": "运行时间",
-      "systemload_average": "系统平均负载",
+      "systemload_average": "1 分钟系统负载",
       "heap": "堆内存",
       "nonheap": "非堆内存",
       "used": "已用",
@@ -1408,13 +1413,15 @@
       "threads_live": "存活线程",
       "heap_used_bytes": "堆内存已用",
       "nonheap_used_bytes": "非堆内存已用",
-      "graphs": "图数",
-      "nodes": "节点数",
-      "backend_counts": "后端统计",
+      "garbage_collection_count": "GC 次数",
+      "garbage_collection_time": "GC 耗时",
+      "graphs": "图数量",
+      "nodes": "后端节点数",
+      "backend_counts": "存储后端",
       "capacity_bytes": "总容量",
       "available_bytes": "可用容量",
       "partitions": "分区数",
-      "leaders": "Leader 数",
+      "leaders": "Leader 分区数",
       "total_space": "总空间",
       "usable_space": "可用空间",
       "free_space": "空闲空间",
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index 39c33392d..9fe7213e7 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -118,17 +118,9 @@ const EditLayer = ({
             }
         };
         if (superAdminChanged && profile.user_password) {
-            await requestUpdate(toPermissionPayload({
-                user_name: profile.user_name,
-                permission_preset: values.is_superadmin
-                    ? PERMISSION_PRESETS.SUPER_ADMIN
-                    : PERMISSION_PRESETS.GS_READ_ONLY,
-            }));
-            await requestUpdate(profile);
-        }
-        else {
-            await requestUpdate(payload);
+            throw new 
Error(t('account.feedback.password_permission_separate'));
         }
+        await requestUpdate(payload);
         message.success(t('common.msg.update_success'));
         onCancel();
         refresh();
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
index 1eee4df43..2f6220873 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
@@ -38,28 +38,27 @@ import {PERMISSION_PRESETS} from './permissionPresets';
 import {loadAllPages, PAGE_ERROR_CONFIG} from './pagedRecords';
 
 const responseRecords = response => response?.data?.records ?? [];
-const errorStatus = value => (
-    value?.response?.data?.status ?? value?.response?.status ?? value?.status
-);
 const errorDetail = value => {
     const response = value?.response ?? value;
     return response?.data?.message ?? response?.message;
 };
-const isMissingAccount = value => {
-    if (errorStatus(value) === 400) {
-        return true;
-    }
+const isMissingAccount = (value, t) => {
     const detail = errorDetail(value);
     if (typeof detail !== 'string') {
         return false;
     }
     const normalized = detail.toLowerCase();
+    const subject = t('account.space_access.member.missing_response_subject')
+        .toLowerCase();
+    const predicate = 
t('account.space_access.member.missing_response_predicate')
+        .toLowerCase();
     return normalized.includes('user or group is not exist')
-        || normalized.includes('account does not exist');
+        || normalized.includes('account does not exist')
+        || (normalized.includes(subject) && normalized.includes(predicate));
 };
 const showMutationError = (error, t) => {
     const detail = errorDetail(error);
-    if (isMissingAccount(error)) {
+    if (isMissingAccount(error, t)) {
         message.error(t('account.space_access.member.account_not_found'));
         return;
     }
@@ -208,6 +207,7 @@ const SpaceAccess = ({
     const [spacesError, setSpacesError] = useState(false);
     const [spacesRevision, setSpacesRevision] = useState(0);
     const spacesRequest = useRef(null);
+    const accountValidation = useRef(null);
     const [memberDialog, setMemberDialog] = useState(null);
     const [missingAccountId, setMissingAccountId] = useState(null);
     const [submitting, setSubmitting] = useState(false);
@@ -354,12 +354,16 @@ const SpaceAccess = ({
         spaces,
     ]);
     const closeMember = useCallback(() => {
+        accountValidation.current = null;
         setMemberDialog(null);
         setMissingAccountId(null);
         memberForm.resetFields();
     }, [memberForm]);
     const validateExistingAccount = useCallback(async (_, value) => {
+        const validation = Symbol('account-validation');
+        accountValidation.current = validation;
         setMissingAccountId(null);
+        memberForm.setFieldValue('user_id', undefined);
         if (!value) {
             return;
         }
@@ -370,6 +374,8 @@ const SpaceAccess = ({
         if (!targetSpace) {
             return;
         }
+        const isCurrent = () => accountValidation.current === validation
+            && memberForm.getFieldValue('account_id') === value;
         let response;
         try {
             response = await api.auth.getSpaceAccount(
@@ -377,7 +383,10 @@ const SpaceAccess = ({
             );
         }
         catch (error) {
-            if (isMissingAccount(error)) {
+            if (!isCurrent()) {
+                return;
+            }
+            if (isMissingAccount(error, t)) {
                 setMissingAccountId(value);
                 throw new Error(accountNotFoundMessage());
             }
@@ -385,9 +394,12 @@ const SpaceAccess = ({
                 t('account.space_access.member.account_check_failed')
             );
         }
+        if (!isCurrent()) {
+            return;
+        }
         const account = response?.data;
         if (response?.status !== 200) {
-            if (isMissingAccount(response)) {
+            if (isMissingAccount(response, t)) {
                 setMissingAccountId(value);
                 throw new Error(accountNotFoundMessage());
             }
@@ -401,6 +413,11 @@ const SpaceAccess = ({
         }
         memberForm.setFieldValue('user_id', account.user_id ?? account.id);
     }, [accountNotFoundMessage, memberForm, t]);
+    const handleAccountIdChange = useCallback(() => {
+        accountValidation.current = null;
+        setMissingAccountId(null);
+        memberForm.setFieldValue('user_id', undefined);
+    }, [memberForm]);
     const startAccountCreation = useCallback(() => {
         if (!missingAccountId || !onCreateAccount) {
             return;
@@ -618,7 +635,7 @@ const SpaceAccess = ({
                                 <Input disabled />
                             </Form.Item>
                             <Form.Item name="user_id" hidden>
-                                <Input />
+                                <Input onChange={handleAccountIdChange} />
                             </Form.Item>
                         </>
                     ) : (
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
index 5d5eabef8..8983d9859 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
@@ -30,7 +30,12 @@ import * as api from '../../api';
 let mockAuthContext;
 
 jest.mock('react-i18next', () => ({
-    useTranslation: () => ({t: key => key}),
+    useTranslation: () => ({
+        t: key => ({
+            'account.space_access.member.missing_response_subject': '账号',
+            'account.space_access.member.missing_response_predicate': '不存在',
+        }[key] ?? key),
+    }),
 }));
 
 jest.mock('../../auth/AuthContext', () => ({
@@ -567,6 +572,109 @@ test('tells a GraphSpace administrator to contact a 
global administrator', async
     })).not.toBeInTheDocument();
 });
 
+test('recognizes a localized missing-account response', async () => {
+    api.auth.getSpaceAccount.mockResolvedValue({
+        status: 400,
+        message: '账号 missing-user 不存在,请先创建账号再分配 GraphSpace 权限。',
+    });
+    render(<SpaceAccess />);
+
+    await screen.findAllByText('alice');
+    fireEvent.click(screen.getByRole('button', {
+        name: 'account.space_access.member.add',
+    }));
+    const dialog = screen.getByRole('dialog');
+    const account = within(dialog).getByLabelText(
+        'account.space_access.member.existing_account'
+    );
+    fireEvent.change(account, {target: {value: 'missing-user'}});
+    fireEvent.blur(account);
+
+    expect(await within(dialog).findByText(
+        'account.space_access.member.account_not_found_contact_admin'
+    )).toBeInTheDocument();
+});
+
+test('does not describe an unrelated bad request as a missing account', async 
() => {
+    api.auth.getSpaceAccount.mockResolvedValue({
+        status: 400,
+        message: 'Invalid permission preset',
+    });
+    render(<SpaceAccess />);
+
+    await screen.findAllByText('alice');
+    fireEvent.click(screen.getByRole('button', {
+        name: 'account.space_access.member.add',
+    }));
+    const dialog = screen.getByRole('dialog');
+    const account = within(dialog).getByLabelText(
+        'account.space_access.member.existing_account'
+    );
+    fireEvent.change(account, {target: {value: 'alice'}});
+    fireEvent.blur(account);
+
+    expect(await within(dialog).findByText(
+        'account.space_access.member.account_check_failed'
+    )).toBeInTheDocument();
+    expect(within(dialog).queryByText(
+        'account.space_access.member.account_not_found_contact_admin'
+    )).not.toBeInTheDocument();
+});
+
+test('ignores an older account lookup after the ID changes', async () => {
+    const older = deferred();
+    const current = deferred();
+    api.auth.getSpaceAccount.mockImplementation((_space, accountId) => (
+        accountId === 'older' ? older.promise : current.promise
+    ));
+    api.auth.setSpacePreset.mockResolvedValue({status: 200});
+    render(<SpaceAccess />);
+
+    await screen.findAllByText('alice');
+    fireEvent.click(screen.getByRole('button', {
+        name: 'account.space_access.member.add',
+    }));
+    const dialog = screen.getByRole('dialog');
+    const account = within(dialog).getByLabelText(
+        'account.space_access.member.existing_account'
+    );
+    fireEvent.change(account, {target: {value: 'older'}});
+    fireEvent.blur(account);
+    fireEvent.change(account, {target: {value: 'current'}});
+    fireEvent.blur(account);
+
+    await act(async () => current.resolve({
+        status: 200,
+        data: {user_id: 'current-id', user_name: 'current'},
+    }));
+    await act(async () => older.resolve({
+        status: 200,
+        data: {user_id: 'older-id', user_name: 'older'},
+    }));
+
+    const roleSelects = within(dialog).getAllByRole('combobox');
+    fireEvent.mouseDown(roleSelects[roleSelects.length - 1]);
+    
fireEvent.click(screen.getByText('account.permission_preset.GS_READ_ONLY'));
+    fireEvent.click(within(dialog).getByRole('button', {
+        name: 'common.action.save',
+    }));
+
+    await waitFor(() => expect(api.auth.setSpacePreset).toHaveBeenCalledWith(
+        'SPACE_A',
+        'current-id',
+        'current',
+        'GS_READ_ONLY',
+        expect.any(Object)
+    ));
+    expect(api.auth.setSpacePreset).not.toHaveBeenCalledWith(
+        'SPACE_A',
+        'older-id',
+        expect.anything(),
+        expect.anything(),
+        expect.any(Object)
+    );
+});
+
 test('requires an explicit replacement for legacy custom access', async () => {
     setResponses({
         members: [{
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
index 2b88966dd..809742a66 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
@@ -317,7 +317,7 @@ test('updates a password without resubmitting unchanged 
permissions', async () =
     expect(payload).not.toHaveProperty('graphspace_permissions');
 });
 
-test('separates simultaneous password and super administrator changes', async 
() => {
+test('requires password and super administrator changes to be separate', async 
() => {
     mockAuthContext = {
         capabilities: ['accounts_manage', 'account_permission_presets'],
     };
@@ -343,20 +343,40 @@ test('separates simultaneous password and super 
administrator changes', async ()
         '.ant-modal-footer .ant-btn-primary'
     ));
 
-    await waitFor(() => expect(api.auth.updateUser).toHaveBeenCalledTimes(2));
-    
expect(api.auth.updateUser.mock.calls[0][1]).toEqual(expect.objectContaining({
-        user_name: 'alice',
-        permission_preset: 'SUPER_ADMIN',
-        is_superadmin: true,
-    }));
-    expect(api.auth.updateUser.mock.calls[0][1])
-        .not.toHaveProperty('user_password');
-    
expect(api.auth.updateUser.mock.calls[1][1]).toEqual(expect.objectContaining({
-        user_name: 'alice',
-        user_password: 'new-password',
-    }));
-    expect(api.auth.updateUser.mock.calls[1][1])
-        .not.toHaveProperty('permission_preset');
+    expect((await screen.findAllByText(
+        'account.feedback.password_permission_separate'
+    )).length).toBeGreaterThan(0);
+    expect(api.auth.updateUser).not.toHaveBeenCalled();
+});
+
+test('rejects a combined profile and promotion before sending it', async () => 
{
+    mockAuthContext = {
+        capabilities: ['accounts_manage', 'account_permission_presets'],
+    };
+    api.auth.getUserInfo.mockResolvedValue({
+        status: 200,
+        data: {
+            user_name: 'alice',
+            is_superadmin: false,
+            permission_preset: 'GS_READ_ONLY',
+            graphspace_permissions: [],
+        },
+    });
+    render(<EditLayer {...props} data={{id: 'alice'}} op='edit' />);
+
+    await screen.findByDisplayValue('alice');
+    fireEvent.change(screen.getByPlaceholderText(
+        'account.form.default_password_placeholder'
+    ), {target: {value: 'new-password'}});
+    fireEvent.click(screen.getByRole('switch'));
+    fireEvent.click(document.querySelector(
+        '.ant-modal-footer .ant-btn-primary'
+    ));
+
+    expect((await screen.findAllByText(
+        'account.feedback.password_permission_separate'
+    )).length).toBeGreaterThan(0);
+    expect(api.auth.updateUser).not.toHaveBeenCalled();
 });
 
 test('keeps GraphSpace membership out of the account profile form', async () 
=> {
diff --git a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.js 
b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.js
index f4db39cd6..f62433a02 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.js
@@ -40,11 +40,12 @@ const TitleField = ({item, onClick, onKeyDown}) => {
         : getResourceDisplayName(item.name, item.nickname);
 
     return (
-        <>
+        <div className={style.card_title}>
             <Typography.Text
-                style={{maxWidth: 244}}
-                ellipsis={{ellipsis: true}}
+                className={style.card_title_text}
+                ellipsis={{tooltip: displayName}}
                 title={`${displayName}`}
+                aria-label={displayName}
                 onClick={onClick}
                 onKeyDown={onKeyDown}
                 role='button'
@@ -57,7 +58,7 @@ const TitleField = ({item, onClick, onKeyDown}) => {
                 )}
                 {item.create_time ? 
moment(item.create_time).format('YYYY-MM-DD') : '--'} 
{t('graphspace.card.created')}
             </div>
-        </>
+        </div>
     );
 };
 
@@ -150,7 +151,7 @@ const GraphSpaceCard = ({
                 backgroundImage: 'linear-gradient(180deg, '
                     + 'rgba(51,136,255,0.10) 0%, rgba(51,136,255,0.00) 100%)',
                 borderBottom: 0,
-                height: 93,
+                minHeight: 93,
                 paddingLeft: 20,
             }}
             bodyStyle={{
diff --git a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.test.js
index bc76cea09..2dc002ede 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/Card.test.js
@@ -167,6 +167,30 @@ test('treats a backend nickname echo as an unset alias', 
() => {
     expect(screen.getByTitle('space')).toBeInTheDocument();
 });
 
+test('keeps a long GraphSpace title accessible without a fixed text width', () 
=> {
+    const displayName = 'A GraphSpace name that is much longer than its card';
+    const item = {
+        name: 'long-space', nickname: displayName, create_time: '2026-07-10',
+        auth: false, max_graph_number: 10, cpu_limit: 2, memory_limit: 4,
+        storage_limit: 100, storage_used: 0, storage_percent: 0,
+    };
+
+    render(
+        <GraphSpaceCard
+            item={item}
+            editGraphspace={jest.fn()}
+            deleteGraphspace={jest.fn()}
+            handleInit={jest.fn()}
+        />
+    );
+
+    const title = screen.getByRole('button', {name: displayName});
+    expect(title).toHaveAttribute('title', displayName);
+    expect(title).toHaveClass('card_title_text');
+    expect(title).not.toHaveStyle({maxWidth: '244px'});
+    
expect(screen.getAllByText('graphspace.card.enter').length).toBeGreaterThan(0);
+});
+
 test('keeps public GraphSpace navigation but hides mutation actions for 
viewers', () => {
     const item = {
         name: 'public', nickname: 'Public', create_time: '2026-07-10',
diff --git a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.js 
b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.js
index c0436dca7..a66608735 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.js
@@ -376,7 +376,7 @@ const GraphSpace = () => {
                                     )}
                                     {data.map(item => {
                                         return (
-                                            <Col span={8} key={item.name}>
+                                            <Col xs={24} lg={12} xxl={8} 
key={item.name}>
                                                 <GraphSpaceCard
                                                     item={item}
                                                     
deleteGraphspace={deleteGraphspace}
@@ -390,7 +390,7 @@ const GraphSpace = () => {
                                         );
                                     })}
                                     {!loading && !listError && 
canCreateGraphspace && (
-                                        <Col span={8} key='add'>
+                                        <Col xs={24} lg={12} xxl={8} key='add'>
                                             <Card
                                                 className={style.add_card}
                                                 onClick={handleCreate}
diff --git a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.module.scss 
b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.module.scss
index 4d55f1814..65bad4496 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.module.scss
+++ b/hugegraph-hubble/hubble-fe/src/pages/GraphSpace/index.module.scss
@@ -37,6 +37,24 @@ div.add_card {
     cursor: pointer;
     height: 100%;
 
+    :global {
+        .ant-card-head-wrapper {
+            align-items: flex-start;
+            column-gap: 12px;
+            flex-wrap: nowrap;
+        }
+
+        .ant-card-head-title {
+            flex: 1 1 auto;
+            min-width: 0;
+        }
+
+        .ant-card-extra {
+            flex: 0 0 auto;
+            margin-left: auto;
+        }
+    }
+
     .card_content {
         height: 144px;
         font-size: 12px;
@@ -46,6 +64,31 @@ div.add_card {
     }
 }
 
+@media (max-width: 420px) {
+    .card {
+        :global {
+            .ant-card-head-wrapper {
+                row-gap: 8px;
+                flex-wrap: wrap;
+            }
+
+            .ant-card-head-title {
+                flex-basis: 100%;
+            }
+        }
+    }
+}
+
+.card_title {
+    min-width: 0;
+    width: 100%;
+}
+
+.card_title_text {
+    display: block;
+    max-width: 100%;
+}
+
 .tooltip {
     :global {
         .ant-tooltip-arrow {
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js
index 7090722a8..c02a39fee 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js
@@ -17,7 +17,7 @@
  */
 
 import {Alert, Button, Descriptions, Progress, Skeleton, Space, Statistic, 
Tooltip} from 'antd';
-import {ArrowLeftOutlined} from '@ant-design/icons';
+import {ArrowLeftOutlined, CheckCircleFilled} from '@ant-design/icons';
 import {useCallback, useEffect, useRef, useState} from 'react';
 import {useLocation, useNavigate, useParams} from 'react-router-dom';
 import {useTranslation} from 'react-i18next';
@@ -44,6 +44,7 @@ const GROUPS_BY_TYPE = {
 const SOURCE_BY_TYPE = {SERVER: 'server', PD: 'pd', STORE: 'stores'};
 
 const EMPTY_STATE_BY_AVAILABILITY = {
+    AVAILABLE: 'metric_no_data',
     UNSUPPORTED: 'unsupported',
     MALFORMED: 'metric_malformed',
     UNAVAILABLE: 'unavailable',
@@ -53,10 +54,24 @@ const BYTE_KEYS = new Set([
     'capacity_bytes', 'available_bytes', 'heap_used_bytes', 
'nonheap_used_bytes',
 ]);
 
+const HIDDEN_METRIC_KEYS = {
+    drive: new Set(['total_space', 'free_space', 'size_unit']),
+    backend: new Set(['capacity_bytes', 'available_bytes']),
+};
+
 const metricLabel = (key, t) => t(`operations.metric_labels.${key}`, {
     defaultValue: key.replaceAll('_', ' '),
 });
 
+const metricGroupName = (group, nodeType, t) => {
+    if (group === 'backend') {
+        return t(nodeType === 'STORE'
+            ? 'operations.metric_store_backend'
+            : 'operations.metric_server_backend');
+    }
+    return t(`operations.metric_${group}`);
+};
+
 const formatBytes = value => {
     const units = ['B', 'KB', 'MB', 'GB', 'TB'];
     let size = Number(value);
@@ -117,9 +132,15 @@ const formatDisplayValue = (key, value, parentKey, values, 
unavailable) => {
         return `${new Intl.NumberFormat(undefined, {maximumFractionDigits: 1})
             .format(Number(value) * 100)}%`;
     }
-    const unit = parentKey === 'garbage_collector' && key.endsWith('_time')
-        ? values.time_unit : (['total_space', 'usable_space', 
'free_space'].includes(key)
-            ? values.size_unit : '');
+    if (parentKey === 'garbage_collector' && key.endsWith('_time')) {
+        return formatMetricValue(
+            new Intl.NumberFormat(undefined, {maximumFractionDigits: 2})
+                .format(Number(value)),
+            values.time_unit
+        );
+    }
+    const unit = ['total_space', 'usable_space', 'free_space'].includes(key)
+        ? values.size_unit : '';
     if (unit) {
         return formatUnitValue(value, unit) ?? unavailable;
     }
@@ -185,30 +206,79 @@ const MemoryUsage = ({label, values, unavailable, t}) => {
     );
 };
 
+const MetricStatistics = ({entries, values}) => {
+    const {t} = useTranslation();
+    const unavailable = t('operations.unavailable');
+    return (
+        <div className='operations-system-statistics 
operations-group-statistics'>
+            {entries.map(([key, value]) => (
+                <div className='operations-statistic-card' key={key}>
+                    <span className='operations-statistic-title'>
+                        {metricLabel(key, t)}
+                    </span>
+                    <div className='operations-statistic-value'>
+                        {value !== null && typeof value === 'object'
+                            && !Array.isArray(value)
+                            ? Object.entries(value).map(([nestedKey, 
nestedValue]) => (
+                                <div key={nestedKey}>
+                                    {metricLabel(nestedKey, t)}: {' '}
+                                    {formatDisplayValue(
+                                        nestedKey, nestedValue, key, value, 
unavailable
+                                    )}
+                                </div>
+                            ))
+                            : formatDisplayValue(
+                                key, value, null, values, unavailable
+                            )}
+                    </div>
+                </div>
+            ))}
+        </div>
+    );
+};
+
+const sumMetricValues = (values, suffix) => {
+    const matching = Object.entries(values ?? {})
+        .filter(([key, value]) => key.endsWith(suffix) && 
Number.isFinite(Number(value)))
+        .map(([, value]) => Number(value));
+    return matching.length > 0
+        ? matching.reduce((total, value) => total + value, 0)
+        : null;
+};
+
 const SystemMetricContent = ({values = {}}) => {
     const {t} = useTranslation();
     const unavailable = t('operations.unavailable');
     const basic = values.basic && typeof values.basic === 'object' ? 
values.basic : {};
     const thread = values.thread && typeof values.thread === 'object' ? 
values.thread : {};
+    const garbageCollector = values.garbage_collector
+        && typeof values.garbage_collector === 'object'
+        ? values.garbage_collector : {};
+    const garbageCollectionCount = sumMetricValues(garbageCollector, '_count');
+    const garbageCollectionTime = sumMetricValues(garbageCollector, '_time');
     const runtimeStats = [
         ['process_cpu_usage', values.process_cpu_usage],
         ['system_cpu_usage', values.system_cpu_usage],
         ['systemload_average', values.systemload_average ?? 
basic.systemload_average],
         ['cpu_count', values.cpu_count ?? basic.processors],
-        ['uptime', basic.uptime],
-        ['uptime_seconds', values.uptime_seconds],
+        ['uptime_seconds', values.uptime_seconds
+            ?? (basic.uptime === undefined || basic.uptime === null
+                ? null : Number(basic.uptime) / 1000)],
+        ['threads_live', values.threads_live ?? thread.count],
+        ['heap_used_bytes', values.heap_used_bytes],
+        ['nonheap_used_bytes', values.nonheap_used_bytes],
+        ['garbage_collection_count', garbageCollectionCount],
+        ['garbage_collection_time', garbageCollectionTime],
     ].filter(([, value]) => value !== undefined && value !== null);
-    const threadStats = ['count', 'daemon', 'peak']
-        .filter(key => thread[key] !== undefined && thread[key] !== null);
     const memoryValues = [
         ['heap_usage', values.heap],
         ['nonheap_usage', values.nonheap],
     ].filter(([, value]) => value && typeof value === 'object');
-    const basicDetails = ['mem_total', 'mem_used']
-        .filter(key => basic[key] !== undefined && basic[key] !== null);
     const handledKeys = new Set([
         'basic', 'heap', 'nonheap', 'thread', 'process_cpu_usage',
         'system_cpu_usage', 'systemload_average', 'cpu_count', 
'uptime_seconds',
+        'threads_live', 'heap_used_bytes', 'nonheap_used_bytes',
+        'garbage_collector',
     ]);
     const supplementalEntries = Object.entries(values)
         .filter(([key]) => !handledKeys.has(key));
@@ -242,60 +312,21 @@ const SystemMetricContent = ({values = {}}) => {
                         <Statistic
                             key={key}
                             title={metricLabel(key, t)}
-                            value={formatDisplayValue(key, value, null, 
values, unavailable)}
+                            value={formatDisplayValue(
+                                key,
+                                value,
+                                key === 'garbage_collection_time'
+                                    ? 'garbage_collector' : null,
+                                key === 'garbage_collection_time'
+                                    ? garbageCollector : values,
+                                unavailable
+                            )}
                         />
                     ))}
                 </div>
             )}
-            {threadStats.length > 0 && (
-                <div
-                    className='operations-system-statistics'
-                    role='group'
-                    aria-label={metricLabel('thread', t)}
-                >
-                    {threadStats.map(key => (
-                        <Statistic
-                            key={key}
-                            title={metricLabel(key, t)}
-                            value={thread[key]}
-                        />
-                    ))}
-                </div>
-            )}
-            {basicDetails.length > 0 && (
-                <div className='operations-system-details'>
-                    {basicDetails.map(key => (
-                        <div key={key}>
-                            {metricLabel(key, t)}: {' '}
-                            {formatDisplayValue(key, basic[key], 'basic', 
basic, unavailable)}
-                        </div>
-                    ))}
-                </div>
-            )}
             {supplementalEntries.length > 0 && (
-                <Descriptions
-                    className='operations-system-supplemental'
-                    layout='vertical'
-                    colon={false}
-                    column={{xxl: 3, xl: 2, lg: 2, md: 1, sm: 1, xs: 1}}
-                >
-                    {supplementalEntries.map(([key, value]) => (
-                        <Descriptions.Item key={key} label={metricLabel(key, 
t)}>
-                            {value !== null && typeof value === 'object' && 
!Array.isArray(value)
-                                ? Object.entries(value).map(([nestedKey, 
nestedValue]) => (
-                                    <div key={nestedKey}>
-                                        {metricLabel(nestedKey, t)}: {' '}
-                                        {formatDisplayValue(
-                                            nestedKey, nestedValue, key, 
value, unavailable
-                                        )}
-                                    </div>
-                                ))
-                                : formatDisplayValue(
-                                    key, value, null, values, unavailable
-                                )}
-                        </Descriptions.Item>
-                    ))}
-                </Descriptions>
+                <MetricStatistics entries={supplementalEntries} 
values={values} />
             )}
         </div>
     );
@@ -305,13 +336,16 @@ const MetricGroup = ({group, name, values, status = {}, 
emptyMessage}) => {
     const {t, i18n} = useTranslation();
     const entries = values && typeof values === 'object' && 
!Array.isArray(values)
         ? Object.entries(values) : [];
+    const visibleEntries = entries.filter(([key]) =>
+        !HIDDEN_METRIC_KEYS[group]?.has(key)
+    );
     const availability = status.availability ?? 'UNSUPPORTED';
     const emptyState = EMPTY_STATE_BY_AVAILABILITY[availability] ?? 
'unavailable';
-    const observed = status.observed_at ? formatObservedAt(
-        status.observed_at, i18n.language, t('operations.unavailable')
-    ) : null;
-    const lastSuccess = status.last_success_at ? formatObservedAt(
-        status.last_success_at, i18n.language, t('operations.unavailable')
+    const lastObservedAt = status.stale
+        ? status.last_success_at ?? status.observed_at
+        : status.observed_at ?? status.last_success_at;
+    const lastObserved = lastObservedAt ? formatObservedAt(
+        lastObservedAt, i18n.language, t('operations.unavailable')
     ) : null;
     const availabilityLabel = 
t(`operations.availability_${availability.toLowerCase()}`, {
         defaultValue: availability,
@@ -332,11 +366,20 @@ const MetricGroup = ({group, name, values, status = {}, 
emptyMessage}) => {
             role='status'
             aria-label={availabilityLabel}
         >
-            {status.fresh && <span>{t('operations.fresh')}</span>}
+            {status.fresh && (
+                <Tooltip title={t('operations.fresh')}>
+                    <CheckCircleFilled
+                        className='operations-metric-fresh'
+                        role='img'
+                        aria-label={t('operations.fresh')}
+                    />
+                </Tooltip>
+            )}
             {status.stale && <span>{t('operations.stale')}</span>}
             {reasonLabel && <span>{reasonLabel}</span>}
-            {observed && <span>{t('operations.observed_at')}: 
{observed}</span>}
-            {lastSuccess && <span>{t('operations.last_success')}: 
{lastSuccess}</span>}
+            {lastObserved && (
+                <span>{t('operations.last_observed')}: {lastObserved}</span>
+            )}
         </div>
     );
     const metricHeader = (
@@ -353,7 +396,7 @@ const MetricGroup = ({group, name, values, status = {}, 
emptyMessage}) => {
         </header>
     );
     const capacity = capacitySummary(values);
-    if (entries.length === 0) {
+    if (visibleEntries.length === 0 && !capacity) {
         return (
             <section className='operations-surface operations-metric-group'>
                 {metricHeader}
@@ -386,31 +429,9 @@ const MetricGroup = ({group, name, values, status = {}, 
emptyMessage}) => {
                     />
                 </div>
             )}
-            {group === 'system' ? <SystemMetricContent values={values} /> : (
-                <Descriptions
-                    layout='vertical'
-                    colon={false}
-                    column={{xxl: 3, xl: 2, lg: 2, md: 1, sm: 1, xs: 1}}
-                >
-                    {entries.map(([key, value]) => (
-                        <Descriptions.Item key={key} label={metricLabel(key, 
t)}>
-                            {value !== null && typeof value === 'object' && 
!Array.isArray(value)
-                                ? Object.entries(value).map(([nestedKey, 
nestedValue]) => (
-                                    <div key={nestedKey}>
-                                        {metricLabel(nestedKey, t)}: {' '}
-                                        {formatDisplayValue(
-                                            nestedKey, nestedValue, key, value,
-                                            t('operations.unavailable')
-                                        )}
-                                    </div>
-                                ))
-                                : formatDisplayValue(
-                                    key, value, null, values, 
t('operations.unavailable')
-                                )}
-                        </Descriptions.Item>
-                    ))}
-                </Descriptions>
-            )}
+            {group === 'system'
+                ? <SystemMetricContent values={values} />
+                : <MetricStatistics entries={visibleEntries} values={values} 
/>}
         </section>
     );
 };
@@ -541,7 +562,7 @@ const NodeDetail = () => {
                         <HealthStatus status={node.status} size='large' />
                     </section>
                     <div className='operations-overall-status'>
-                        <span>{t('operations.observed_at')}: {observed}</span>
+                        <span>{t('operations.last_observed')}: 
{observed}</span>
                         {data?.stale && 
<strong>{t('operations.stale')}</strong>}
                     </div>
                 </div>
@@ -587,7 +608,7 @@ const NodeDetail = () => {
                         <MetricGroup
                             key={group}
                             group={group}
-                            name={t(`operations.metric_${group}`)}
+                            name={metricGroupName(group, node.type, t)}
                             values={node.metrics?.[group]}
                             status={status}
                         />
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js
index a2f5d7a89..91ef6c4cb 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js
@@ -52,6 +52,13 @@ test('focuses standalone Server details on applicable 
sources and metric cards',
                     heap: {used: 512, max: 1024, committed: 768},
                     nonheap: {used: 256, max: 0, committed: 320},
                     thread: {count: 42, daemon: 20, peak: 56},
+                    garbage_collector: {
+                        g1_young_generation_count: 3,
+                        g1_young_generation_time: 12,
+                        g1_old_generation_count: 1,
+                        g1_old_generation_time: 4,
+                        time_unit: 'ms',
+                    },
                     process_cpu_usage: 0.125,
                     system_cpu_usage: 0.25,
                 },
@@ -107,23 +114,18 @@ test('focuses standalone Server details on applicable 
sources and metric cards',
     expect(within(system).getByText(/Committed:.*768 MB/)).toBeInTheDocument();
     expect(within(system).queryByText(/NaN|Infinity/)).not.toBeInTheDocument();
 
-    const threads = within(system).getByRole('group', {name: 'Threads'});
-    expect(within(threads).getByText('Live')).toBeInTheDocument();
-    expect(within(threads).getByText('42')).toBeInTheDocument();
-    expect(within(threads).getByText('Daemon')).toBeInTheDocument();
-    expect(within(threads).getByText('20')).toBeInTheDocument();
-    expect(within(threads).getByText('Peak')).toBeInTheDocument();
-    expect(within(threads).getByText('56')).toBeInTheDocument();
-
-    expect(within(system).getByRole('group', {name: 'CPU and runtime'}))
-        .toHaveTextContent('12.5%');
-    expect(within(system).getByRole('group', {name: 'CPU and runtime'}))
-        .toHaveTextContent('25%');
-    expect(within(system).getByRole('group', {name: 'CPU and runtime'}))
-        .toHaveTextContent('1.5');
-    expect(within(system).getByRole('group', {name: 'CPU and runtime'}))
-        .toHaveTextContent('2m 9s');
-    
expect(system.querySelectorAll('.ant-statistic').length).toBeGreaterThanOrEqual(7);
+    const runtime = within(system).getByRole('group', {name: 'CPU and 
runtime'});
+    expect(runtime).toHaveTextContent('12.5%');
+    expect(runtime).toHaveTextContent('25%');
+    expect(runtime).toHaveTextContent('1-minute system load1.5');
+    expect(runtime).not.toHaveTextContent('1.5%');
+    expect(runtime).toHaveTextContent('2m 9s');
+    expect(runtime).toHaveTextContent('Live threads42');
+    expect(runtime).not.toHaveTextContent('Daemon');
+    expect(runtime).not.toHaveTextContent('Peak');
+    expect(runtime).toHaveTextContent('GC count4');
+    expect(runtime).toHaveTextContent('GC time16 ms');
+    
expect(system.querySelectorAll('.ant-statistic').length).toBeGreaterThanOrEqual(8);
 });
 
 afterEach(() => jest.clearAllMocks());
@@ -185,7 +187,7 @@ test('keeps null metrics safe and distinguishes unavailable 
groups', async () =>
     expect(within(identity).getByLabelText('STORE icon')).toBeInTheDocument();
     expect(within(identity).getByText('Store A')).toBeInTheDocument();
     expect(screen.getByRole('region', {name: 'Node 
metrics'})).toBeInTheDocument();
-    expect(screen.getByText(/Observed:/)).toBeInTheDocument();
+    expect(screen.getAllByText(/Last observed:/).length).toBeGreaterThan(0);
     expect(screen.getByRole('button', {name: 'Refresh'})).toHaveClass(
         'operations-refresh-button', 'ant-btn-text', 'ant-btn-circle'
     );
@@ -298,8 +300,8 @@ test('renders each metric group from its own metric 
status', async () => {
                 },
                 drive: {
                     availability: 'UNAVAILABLE',
-                    observed_at: 1000,
-                    last_success_at: 900,
+                    observed_at: 2000,
+                    last_success_at: 1000,
                     fresh: false,
                     stale: true,
                     reason: 'refresh_failed',
@@ -333,7 +335,14 @@ test('renders each metric group from its own metric 
status', async () => {
     expect(within(drive).getByText('Unavailable')).toBeInTheDocument();
     expect(within(drive).getByText('7')).toBeInTheDocument();
     expect(within(drive).getByText(/Stale/)).toBeInTheDocument();
-    expect(within(drive).getByText(/Last success/)).toBeInTheDocument();
+    expect(within(drive).getByText(/Last observed/)).toBeInTheDocument();
+    const formatter = new Intl.DateTimeFormat('en-US', {
+        dateStyle: 'medium',
+        timeStyle: 'medium',
+    });
+    expect(drive).toHaveTextContent(formatter.format(new Date(1000)));
+    expect(drive).not.toHaveTextContent(formatter.format(new Date(2000)));
+    expect(within(drive).queryByText(/Last success/)).not.toBeInTheDocument();
 
     const raft = screen.getByRole('heading', {name: 
'Raft'}).closest('section');
     expect(within(raft).getByText('Unsupported')).toBeInTheDocument();
@@ -345,12 +354,18 @@ test('renders each metric group from its own metric 
status', async () => {
         'Upgrade HugeGraph to a version that provides this metric'
     )).toBeInTheDocument();
 
-    const backend = screen.getByRole('heading', {name: 
'Backend'}).closest('section');
+    const backend = screen.getByRole('heading', {
+        name: 'Storage & partitions',
+    }).closest('section');
     expect(within(backend).getByText('Available')).toBeInTheDocument();
     expect(within(backend).getByText('2')).toBeInTheDocument();
     expect(within(backend).queryByText('Refresh 
failed')).not.toBeInTheDocument();
+    expect(within(backend).getByLabelText('Fresh')).toBeInTheDocument();
+    expect(within(backend).queryByText('Fresh')).not.toBeInTheDocument();
+    expect(within(backend).getAllByText(/Last observed/)).toHaveLength(1);
+    expect(within(backend).queryByText(/Last 
success/)).not.toBeInTheDocument();
 
-    for (const name of ['System', 'Drive', 'Raft', 'Backend']) {
+    for (const name of ['System', 'Drive', 'Raft', 'Storage & partitions']) {
         const group = screen.getByRole('heading', {name}).closest('section');
         
expect(group.querySelector('.operations-metric-header')).toBeInTheDocument();
         expect(within(group).getByRole('status')).toBeInTheDocument();
@@ -385,7 +400,8 @@ test('hides metric groups that do not apply to a PD node', 
async () => {
 
     expect(screen.queryByRole('heading', {name: 
'Drive'})).not.toBeInTheDocument();
     expect(screen.queryByRole('heading', {name: 
'Raft'})).not.toBeInTheDocument();
-    expect(screen.queryByRole('heading', {name: 
'Backend'})).not.toBeInTheDocument();
+    expect(screen.queryByRole('heading', {name: 'Storage & partitions'}))
+        .not.toBeInTheDocument();
     expect(screen.queryByText('Unsupported by this service 
version')).not.toBeInTheDocument();
 });
 
@@ -396,10 +412,21 @@ test('presents native metric labels, units and capacity 
instead of raw keys', as
             ...response.node,
             metrics: {
                 system: {
-                    basic: {mem_total: 64, mem_used: 46, uptime: 128889},
+                    basic: {
+                        mem_total: 64,
+                        mem_used: 46,
+                        uptime: 128889,
+                        systemload_average: 2.33,
+                    },
                     process_cpu_usage: 0.125,
                     uptime_seconds: 65,
-                    garbage_collector: {young_count: 3},
+                    garbage_collector: {
+                        young_count: 3,
+                        young_time: 8,
+                        old_count: 1,
+                        old_time: 2,
+                        time_unit: 'ms',
+                    },
                 },
                 drive: {
                     total_space: 233752,
@@ -407,7 +434,12 @@ test('presents native metric labels, units and capacity 
instead of raw keys', as
                     free_space: 5802,
                     size_unit: 'MB',
                 },
-                backend: {capacity_bytes: 4096, available_bytes: 1024},
+                backend: {
+                    capacity_bytes: 4096,
+                    available_bytes: 1024,
+                    partitions: 12,
+                    leaders: 3,
+                },
             },
         },
     });
@@ -415,13 +447,18 @@ test('presents native metric labels, units and capacity 
instead of raw keys', as
     renderDetail();
     await screen.findByRole('heading', {name: 'Store A'});
 
-    expect(screen.getByText(/Total memory:.*64 MB/)).toBeInTheDocument();
+    expect(screen.queryByText(/Total memory/)).not.toBeInTheDocument();
+    expect(screen.queryByText(/Used memory/)).not.toBeInTheDocument();
     const runtime = screen.getByRole('group', {name: 'CPU and runtime'});
     expect(runtime).toHaveTextContent('Uptime');
-    expect(runtime).toHaveTextContent('2m 9s');
+    expect(runtime).toHaveTextContent('1-minute system load');
+    expect(runtime).toHaveTextContent('2.33');
+    expect(runtime).not.toHaveTextContent('2.33%');
     expect(screen.getByText('12.5%')).toBeInTheDocument();
     expect(screen.getByText('1m 5s')).toBeInTheDocument();
-    expect(screen.getByText(/young count:.*3/i)).toBeInTheDocument();
+    expect(screen.queryByText('2m 9s')).not.toBeInTheDocument();
+    expect(runtime).toHaveTextContent('GC count4');
+    expect(runtime).toHaveTextContent('GC time10 ms');
     expect(screen.queryByText(/mem total/)).not.toBeInTheDocument();
     const capacity = screen.getAllByRole('progressbar', {name: 'Capacity 
usage'});
     expect(capacity.some(item => item.getAttribute('aria-valuenow') === 
'75')).toBe(true);
@@ -429,4 +466,78 @@ test('presents native metric labels, units and capacity 
instead of raw keys', as
     expect(screen.getByText('75%')).toBeInTheDocument();
     expect(screen.getByText('98%')).toBeInTheDocument();
     expect(screen.getByText(/222.6 GB \/ 228.3 GB/)).toBeInTheDocument();
+    const drive = screen.getByRole('heading', {name: 
'Drive'}).closest('section');
+    expect(within(drive).getByText('Available space')).toBeInTheDocument();
+    expect(within(drive).getByText('5.7 GB')).toBeInTheDocument();
+    expect(within(drive).queryByText('Free space')).not.toBeInTheDocument();
+    expect(within(drive).queryByText('Space unit')).not.toBeInTheDocument();
+    expect(within(drive).getByText('Available space')
+        .closest('.operations-statistic-card')).toBeInTheDocument();
+    const backend = screen.getByRole('heading', {
+        name: 'Storage & partitions',
+    }).closest('section');
+    expect(within(backend).getByText('Partition count')).toBeInTheDocument();
+    expect(within(backend).getByText('Leader partitions')).toBeInTheDocument();
+    expect(within(backend).queryByText('Total 
capacity')).not.toBeInTheDocument();
+    expect(within(backend).queryByText('Available 
capacity')).not.toBeInTheDocument();
+});
+
+test('uses concise Chinese names for Raft and Store storage metrics', async () 
=> {
+    i18n.changeLanguage('zh-CN');
+    getNode.mockResolvedValue({
+        ...response,
+        node: {
+            ...response.node,
+            metrics: {
+                system: {},
+                drive: {total_space: 100, usable_space: 40, size_unit: 'MB'},
+                raft: {groups: 12, enabled_groups: 12},
+                backend: {
+                    capacity_bytes: 100,
+                    available_bytes: 40,
+                    partitions: 12,
+                    leaders: 3,
+                },
+            },
+            metric_statuses: {
+                system: {availability: 'AVAILABLE', observed_at: 1000},
+                drive: {availability: 'AVAILABLE', observed_at: 1000},
+                raft: {availability: 'AVAILABLE', observed_at: 1000},
+                backend: {availability: 'AVAILABLE', observed_at: 1000},
+            },
+        },
+    });
+
+    renderDetail();
+
+    expect(await screen.findByRole('heading', {name: 'Raft'}))
+        .toBeInTheDocument();
+    expect(screen.getByRole('heading', {name: '存储与分区'}))
+        .toBeInTheDocument();
+    expect(screen.queryByText('Raft 共识')).not.toBeInTheDocument();
+});
+
+test('shows an empty state when a metric group only has hidden metadata', 
async () => {
+    getNode.mockResolvedValue({
+        ...response,
+        node: {
+            ...response.node,
+            metrics: {
+                system: {},
+                drive: {total_space: 100, free_space: 40, size_unit: 'MB'},
+            },
+            metric_statuses: {
+                system: {availability: 'AVAILABLE', observed_at: 1000},
+                drive: {availability: 'AVAILABLE', observed_at: 1000},
+            },
+        },
+    });
+
+    renderDetail();
+
+    const drive = (await screen.findByRole('heading', {name: 'Drive'}))
+        .closest('section');
+    expect(within(drive).getByRole('note')).toHaveTextContent('No metric 
data');
+    expect(drive.querySelector('.operations-group-statistics'))
+        .not.toBeInTheDocument();
 });
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.js
index e3cd98d7f..e21beea13 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.js
@@ -190,6 +190,11 @@ const Overview = () => {
     const nodes = Array.isArray(data?.nodes) ? data.nodes : [];
     const attentionNodes = selectAttentionNodes(nodes);
     const attentionSources = selectAttentionSources(data?.sources);
+    const clusterReason = data?.reason?.startsWith('cluster_')
+        ? data.reason : null;
+    const showAttention = nodes.length > 0
+        || attentionSources.length > 0
+        || clusterReason;
     const facts = data?.facts ?? {};
     const pdLeader = facts.pd_leader ?? nodes.find(node => (
         node.type === 'PD' && node.role === 'LEADER'
@@ -388,14 +393,11 @@ const Overview = () => {
                         <div className='operations-overview-grid'>
                             <section className='operations-topology-surface'>
                                 <div className='operations-section-heading'>
-                                    <div>
-                                        <h3>
-                                            {view === 'topology'
-                                                ? t('operations.topology')
-                                                : 
t('operations.node_list_view')}
-                                        </h3>
-                                        
<span>{t('operations.logical_relationship')}</span>
-                                    </div>
+                                    <h3>
+                                        {view === 'topology'
+                                            ? t('operations.topology')
+                                            : t('operations.node_list_view')}
+                                    </h3>
                                 </div>
                                 {nodes.length === 0
                                     ? <Empty 
description={t('operations.empty_cluster')} />
@@ -476,7 +478,7 @@ const Overview = () => {
                         </div>
                         <SourceStrip sources={data?.sources} />
                     </section>
-                    {nodes.length > 0 && (
+                    {showAttention && (
                         <section
                             className='operations-surface operations-attention'
                             aria-label={t('operations.attention_items')}
@@ -487,6 +489,26 @@ const Overview = () => {
                                     {t('operations.view_all_nodes')} 
<RightOutlined />
                                 </Link>
                             </div>
+                            {clusterReason && (
+                                <Alert
+                                    className='operations-source-attention'
+                                    type='warning'
+                                    showIcon
+                                    message={t('operations.cluster_attention')}
+                                    description={t(
+                                        `operations.reason_${clusterReason}`,
+                                        {
+                                            defaultValue: clusterReason
+                                                .replaceAll('_', ' '),
+                                        }
+                                    )}
+                                    action={(
+                                        <Link to='/operations/nodes'>
+                                            {t('operations.view_all_nodes')}
+                                        </Link>
+                                    )}
+                                />
+                            )}
                             {attentionSources.map(source => {
                                 const type = source.name === 'stores'
                                     ? 'STORE' : source.name.toUpperCase();
@@ -527,7 +549,7 @@ const Overview = () => {
                                 />
                             ) : (
                                 <p className='operations-nodes-healthy'>
-                                    {attentionSources.length > 0
+                                    {(attentionSources.length > 0 || 
clusterReason)
                                         ? 
t('operations.nodes_healthy_source_attention')
                                         : t('operations.all_nodes_healthy')}
                                 </p>
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js
index 54f777df9..aecd00c3a 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js
@@ -484,7 +484,7 @@ test('keeps healthy freshness compact but preserves 
stale-source recovery contex
     const sources = await screen.findByRole('region', {name: 'Source 
freshness'});
     expect(within(sources).queryByText(/Last 
success/)).not.toBeInTheDocument();
     expect(within(sources).getByText(/Stale/)).toBeInTheDocument();
-    expect(within(sources).getByLabelText(/Stale.*Last success/))
+    expect(within(sources).getByLabelText(/Last observed.*Stale/))
         .toBeInTheDocument();
 });
 
@@ -505,14 +505,11 @@ test('shows a concise healthy state when no node needs 
attention', async () => {
 test('explains PD cluster attention while keeping healthy nodes explicit', 
async () => {
     getOverview.mockResolvedValue({
         status: 'DEGRADED',
+        reason: 'cluster_not_ready',
         observed_at: 1000,
         sources: {
             server: {status: 'UP', availability: 'AVAILABLE'},
-            pd: {
-                status: 'DEGRADED',
-                availability: 'AVAILABLE',
-                reason: 'cluster_not_ready',
-            },
+            pd: {status: 'UP', availability: 'AVAILABLE'},
             stores: {status: 'UP', availability: 'AVAILABLE'},
         },
         facts: {},
@@ -539,18 +536,46 @@ test('explains PD cluster attention while keeping healthy 
nodes explicit', async
     const attention = await screen.findByRole('region', {
         name: 'Items needing attention',
     });
-    expect(within(attention).getByText('PD source needs attention'))
+    expect(within(attention).getByText('Cluster state needs attention'))
         .toBeInTheDocument();
     expect(within(attention).getByText(/PD reports Cluster_Not_Ready/))
         .toBeInTheDocument();
-    expect(within(attention).getByRole('link', {name: 'View PD nodes'}))
-        .toHaveAttribute('href', '/operations/nodes?type=PD');
+    expect(within(attention).getByRole('link', {name: 'View all nodes'}))
+        .toHaveAttribute('href', '/operations/nodes');
     expect(within(attention).getByText(
         'All nodes are healthy; the source or cluster state above is a 
separate signal.'
     )).toBeInTheDocument();
     expect(within(attention).queryByRole('table')).not.toBeInTheDocument();
 });
 
+test('shows failed sources even when discovery returned no nodes', async () => 
{
+    getOverview.mockResolvedValue({
+        status: 'DOWN',
+        observed_at: 1000,
+        sources: {
+            server: {
+                status: 'DOWN',
+                availability: 'UNAVAILABLE',
+                reason: 'upstream_unavailable',
+            },
+            pd: {status: 'UNSUPPORTED', availability: 'UNSUPPORTED'},
+            stores: {status: 'UNSUPPORTED', availability: 'UNSUPPORTED'},
+        },
+        facts: {},
+        nodes: [],
+    });
+
+    renderOverview();
+
+    const attention = await screen.findByRole('region', {
+        name: 'Items needing attention',
+    });
+    expect(within(attention).getByText('Server source needs attention'))
+        .toBeInTheDocument();
+    expect(within(attention).getByText('Upstream unavailable'))
+        .toBeInTheDocument();
+});
+
 test('switches between the topology and an accessible node list', async () => {
     getOverview.mockResolvedValue({
         status: 'UP',
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js
index 2bd2c70b7..c5bd18891 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js
@@ -136,13 +136,16 @@ const SourceStrip = ({sources = {}, detailed = false,
         >
             {sourceNames.map(name => {
                 const source = sources[name] ?? {};
-                const age = source.observed_at ? formatObservedAge(
-                    source.observed_at,
+                const lastObservedAt = source.stale
+                    ? source.last_success_at ?? source.observed_at
+                    : source.observed_at ?? source.last_success_at;
+                const age = lastObservedAt ? formatObservedAge(
+                    lastObservedAt,
                     i18n.language,
                     t('operations.unavailable')
                 ) : null;
-                const observed = source.observed_at ? formatObservedAt(
-                    source.observed_at,
+                const observed = lastObservedAt ? formatObservedAt(
+                    lastObservedAt,
                     i18n.language,
                     t('operations.unavailable')
                 ) : null;
@@ -152,22 +155,16 @@ const SourceStrip = ({sources = {}, detailed = false,
                     defaultValue: source.availability ?? 'UNSUPPORTED',
                 });
                 const healthSummary = sourceHealthSummary(source, t);
-                const lastSuccess = source.last_success_at ? formatObservedAt(
-                    source.last_success_at,
-                    i18n.language,
-                    t('operations.unavailable')
-                ) : null;
                 const sourceDetails = [
                     healthSummary,
                     `${t('operations.source_topology_status')}: ${
                         displayHealthStatus(source.status ?? 'UNKNOWN', t)
                     }`,
                     `${t('operations.source_collection_status')}: 
${availability}`,
-                    observed ? `${t('operations.observed_at')}: ${observed}` : 
null,
+                    observed
+                        ? `${t('operations.last_observed')}: ${observed}` : 
null,
                     source.stale ? t('operations.stale') : null,
                     source.reason ? formatReason(source.reason, t) : null,
-                    lastSuccess
-                        ? `${t('operations.last_success')}: ${lastSuccess}` : 
null,
                 ].filter(Boolean).join(' · ');
                 const sourceLabel = displayNodeType(name === 'stores'
                     ? 'STORE' : name.toUpperCase());
@@ -206,14 +203,13 @@ const SourceStrip = ({sources = {}, detailed = false,
                                         availability
                                     }`}
                                     {observed
-                                        ? ` · ${t('operations.observed_at')}: 
${observed}`
+                                        ? ` · 
${t('operations.last_observed')}: ${
+                                            observed
+                                        }`
                                         : (age ? ` · ${age}` : '')}
                                     {source.stale ? ` · 
${t('operations.stale')}` : ''}
                                     {source.reason
                                         ? ` · ${formatReason(source.reason, 
t)}` : ''}
-                                    {lastSuccess
-                                        ? ` · ${t('operations.last_success')}: 
${lastSuccess}`
-                                        : ''}
                                 </span>
                             )}
                         </div>
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js
index 562647b91..6cf5b4d8c 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js
@@ -60,6 +60,32 @@ test('localizes the standalone deployment reason code', 
async () => {
         .not.toBeInTheDocument();
 });
 
+test('uses the last successful data time for a stale source', () => {
+    render(
+        <SourceStrip
+            detailed
+            sources={{
+                stores: {
+                    status: 'UP',
+                    availability: 'PARTIAL',
+                    stale: true,
+                    observed_at: 2000,
+                    last_success_at: 1000,
+                },
+            }}
+            sourceNames={['stores']}
+        />
+    );
+
+    const source = screen.getByText('Store').closest('.operations-source');
+    const formatter = new Intl.DateTimeFormat('en-US', {
+        dateStyle: 'medium',
+        timeStyle: 'medium',
+    });
+    expect(source).toHaveTextContent(`Last observed: ${formatter.format(new 
Date(1000))}`);
+    expect(source).not.toHaveTextContent(formatter.format(new Date(2000)));
+});
+
 test('uses a concise Attention label and explains the degraded state', () => {
     render(<HealthStatus status='DEGRADED' reason='refresh_failed' />);
 
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss
index 13232b3e9..9cdb685dd 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss
@@ -644,12 +644,18 @@
 
 .operations-metric-status {
     display: flex;
+    align-items: center;
     flex-wrap: wrap;
     gap: 4px 10px;
     color: var(--workbench-color-text-secondary);
     font-size: 12px;
 }
 
+.operations-metric-fresh {
+    color: #237804;
+    font-size: 14px;
+}
+
 .operations-metric-empty {
     display: flex;
     align-items: center;
@@ -699,8 +705,9 @@
 .operations-memory-usage {
     min-width: 0;
     padding: 12px 14px;
+    border: 1px solid var(--workbench-color-border);
     border-radius: var(--workbench-radius);
-    background: var(--workbench-color-canvas);
+    background: var(--workbench-color-surface);
 
     > div:first-child {
         display: flex;
@@ -726,7 +733,8 @@
     grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
     gap: 10px;
 
-    .ant-statistic {
+    .ant-statistic,
+    .operations-statistic-card {
         min-width: 0;
         padding: 10px 12px;
         border: 1px solid var(--workbench-color-border);
@@ -739,11 +747,27 @@
     }
 }
 
-.operations-system-details {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px 20px;
+.operations-group-statistics {
+    padding-top: 16px;
+}
+
+.operations-statistic-title {
+    display: block;
     color: var(--workbench-color-text-secondary);
+    font-size: 14px;
+    line-height: 22px;
+}
+
+.operations-statistic-value {
+    margin-top: 4px;
+    color: var(--workbench-color-text);
+    font-size: 20px;
+    line-height: 30px;
+
+    > div {
+        font-size: 14px;
+        line-height: 22px;
+    }
 }
 
 section.operations-metric-group .ant-descriptions-view 
.ant-descriptions-item-content {

Reply via email to