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


The following commit(s) were added to refs/heads/master by this push:
     new eac4707dd feat(hubble): use bearer token for authenticated clients 
(#753)
eac4707dd is described below

commit eac4707dd8d252aa0500808893c3226a80dc3797
Author: imbajin <[email protected]>
AuthorDate: Fri Aug 7 23:10:39 2026 +0800

    feat(hubble): use bearer token for authenticated clients (#753)
    
    - send Hubble JWT credentials with the standard Bearer scheme
    - preserve Bearer authentication when reusing request-scoped clients
    - verify the real HugeClient request header sent to the Gremlin endpoint
---
 hugegraph-hubble/Dockerfile                        |  13 ++
 .../hugegraph/controller/BaseController.java       |   7 +-
 .../controller/ingest/IngestController.java        |  45 +++++-
 .../controller/saas/SaaSMetricsController.java     |   4 +-
 .../hugegraph/service/graphs/GraphsService.java    |  13 +-
 .../hugegraph/service/load/LoadTaskService.java    |   1 +
 .../hugegraph/service/space/GraphSpaceService.java |  27 +++-
 .../org/apache/hugegraph/util/HugeClientUtil.java  |  38 ++++-
 .../controller/ingest/IngestControllerTest.java    | 173 +++++++++++++++++++++
 .../controller/saas/SaaSMetricsControllerTest.java |  74 +++++++++
 .../service/space/GraphSpaceServiceTest.java       |  41 +++++
 .../unit/BaseControllerGremlinClientTest.java      |  42 +++++
 .../hugegraph/unit/GraphsServiceDefaultTest.java   |  30 ++++
 .../apache/hugegraph/unit/LoadTaskServiceTest.java |  34 +++-
 .../apache/hugegraph/util/HugeClientUtilTest.java  | 153 ++++++++++++++++++
 .../src/test/python/check_server_status.feature    |   2 +-
 .../src/test/python/steps/check_server_status.py   |  12 +-
 .../assembly/static/bin/common_functions           |  29 +++-
 .../assembly/static/bin/start-hubble.sh            |   6 +-
 .../static/conf/hugegraph-hubble.properties        |   4 +-
 .../assembly/travis/run_live_hubble_smoke.py       |  22 +--
 .../assembly/travis/run_ui_browser_smoke.js        | 126 +++++++++++----
 .../assembly/travis/run_ui_browser_smoke.test.js   | 143 +++++++++++++++++
 .../hubble-dist/assembly/travis/start-hubble.sh    |   6 +-
 .../assembly/travis/test_run_live_hubble_smoke.py  |  36 ++++-
 .../src/i18n/empty-result-copy-context.test.js     |  66 ++++++++
 .../src/i18n/resources/en-US/modules/analysis.json |   3 +-
 .../src/i18n/resources/zh-CN/modules/analysis.json |   3 +-
 .../modules/algorithm/GraphResult/Home/index.js    |   2 +-
 .../algorithm/GraphResult/RankApiView/index.js     |   2 +-
 .../hugegraph/loader/executor/LoadOptions.java     |   7 +-
 .../loader/reader/file/FileLineFetcher.java        |   1 +
 .../hugegraph/loader/source/file/FileSource.java   |  36 ++++-
 .../loader/test/unit/FileLineFetcherTest.java      | 110 +++++++++++++
 .../loader/test/unit/LoadOptionsTest.java          |  27 ++++
 35 files changed, 1236 insertions(+), 102 deletions(-)

diff --git a/hugegraph-hubble/Dockerfile b/hugegraph-hubble/Dockerfile
index 97f72ed57..b1d5b3c19 100644
--- a/hugegraph-hubble/Dockerfile
+++ b/hugegraph-hubble/Dockerfile
@@ -38,7 +38,20 @@ RUN set -x \
 
 FROM eclipse-temurin:11-jre-jammy
 
+RUN apt-get -q update \
+    && apt-get install --no-install-recommends curl -yq \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
 COPY --from=build /pkg/hugegraph-hubble/apache-hugegraph-hubble-*/ /hubble
+RUN sed -i \
+    -e 's/^server\.host=.*/server.host=0.0.0.0/' \
+    -e 's/^dashboard\.address=.*/dashboard.address=/' \
+    /hubble/conf/hugegraph-hubble.properties \
+    && grep -Fqx 'server.host=0.0.0.0' \
+        /hubble/conf/hugegraph-hubble.properties \
+    && grep -Fqx 'dashboard.address=' \
+        /hubble/conf/hugegraph-hubble.properties
 WORKDIR /hubble/
 
 # SECURITY: This is a plain HTTP port. Do not publish it to an untrusted 
network;
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
index 1d2fc210b..807da39fc 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
@@ -42,6 +42,7 @@ import org.apache.hugegraph.common.Identifiable;
 import org.apache.hugegraph.common.Mergeable;
 import org.apache.hugegraph.util.EntityUtil;
 import org.apache.hugegraph.util.Ex;
+import org.apache.hugegraph.util.HugeClientUtil;
 
 @Component
 public abstract class BaseController {
@@ -196,7 +197,11 @@ public abstract class BaseController {
         HttpServletRequest request = getRequest();
         if (request.getAttribute("hugeClient") != null) {
             HugeClient client = (HugeClient) 
request.getAttribute("hugeClient");
-            client.setAuthContext("Basic " + this.getToken());
+            String token = this.getToken();
+            if (org.apache.commons.lang3.StringUtils.isNotBlank(token)) {
+                client.setAuthContext(
+                        HugeClientUtil.bearerAuthContext(token));
+            }
             return client;
         }
         HugeClient client = 
this.hugeClientPoolService.createTempTokenClient(this.getToken());
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
index d0d6c201c..b464063c4 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
@@ -19,6 +19,9 @@ package org.apache.hugegraph.controller.ingest;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.Data;
 import lombok.extern.log4j.Log4j2;
 import org.apache.commons.io.FileUtils;
@@ -45,6 +48,7 @@ import org.apache.hugegraph.entity.load.NullValues;
 import org.apache.hugegraph.entity.load.ValueMappingItem;
 import org.apache.hugegraph.entity.load.VertexMapping;
 import org.apache.hugegraph.exception.InternalException;
+import org.apache.hugegraph.loader.source.file.FileFormat;
 import org.apache.hugegraph.options.HubbleOptions;
 import org.apache.hugegraph.service.load.DatasourceService;
 import org.apache.hugegraph.service.load.FileMappingService;
@@ -76,6 +80,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -88,6 +93,10 @@ import java.util.stream.Collectors;
 @RequestMapping(Constant.API_VERSION + "ingest")
 public class IngestController extends BaseController {
 
+    private static final ObjectMapper JSON_MAPPER =
+            new ObjectMapper().disable(
+                    JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION);
+
     @Autowired
     private JobManagerService jobManagerService;
     @Autowired
@@ -193,10 +202,11 @@ public class IngestController extends BaseController {
                            .data(header).build();
         }
 
+        boolean hasPhysicalHeader = this.hasPhysicalHeader(config);
         FileSetting setting = this.buildFileSetting(config, 
Collections.emptyList(),
-                                                    false);
+                                                    hasPhysicalHeader);
         ColumnInfo columns = this.readColumns(this.requireUploadFile(config),
-                                             setting, false);
+                                             setting, hasPhysicalHeader);
         return Response.builder().status(Constant.STATUS_OK)
                        .data(columns.names).build();
     }
@@ -234,8 +244,7 @@ public class IngestController extends BaseController {
         File sourceFile = this.requireUploadFile(input);
         long totalSize = sourceFile.length();
         List<String> header = this.stringList(input.get("header"));
-        boolean hasPhysicalHeader = !this.stringList(dsConfig.get("header"))
-                                     .isEmpty();
+        boolean hasPhysicalHeader = this.hasPhysicalHeader(dsConfig);
         FileSetting setting = this.buildFileSetting(input, header,
                                                     hasPhysicalHeader);
         if (setting.getColumnNames() == null ||
@@ -548,6 +557,13 @@ public class IngestController extends BaseController {
         return setting;
     }
 
+    private boolean hasPhysicalHeader(Map<String, Object> config) {
+        FileFormat format = FileFormat.valueOf(
+                this.stringOrDefault(config.get("format"), "CSV"));
+        return format.needHeader() &&
+               this.stringList(config.get("header")).isEmpty();
+    }
+
     private ColumnInfo readColumns(File file, FileSetting setting,
                                    boolean hasPhysicalHeader) {
         try (BufferedReader reader = Files.newBufferedReader(file.toPath())) {
@@ -559,6 +575,10 @@ public class IngestController extends BaseController {
             }
             Ex.check(line != null, "The file has no data line can treat as 
header");
 
+            if (FileFormat.JSON.name().equals(setting.getFormat())) {
+                return this.readJsonColumns(line, file);
+            }
+
             List<String> firstLine = this.splitLine(line, 
setting.getDelimiter());
             if (hasPhysicalHeader) {
                 String sample = reader.readLine();
@@ -579,6 +599,23 @@ public class IngestController extends BaseController {
         }
     }
 
+    private ColumnInfo readJsonColumns(String line, File file) {
+        try {
+            Map<String, Object> fields = JSON_MAPPER.readValue(
+                    line, new TypeReference<LinkedHashMap<String, Object>>() {
+                    });
+            List<String> names = new ArrayList<>(fields.keySet());
+            List<String> values = names.stream()
+                                       .map(fields::get)
+                                       .map(String::valueOf)
+                                       .collect(Collectors.toList());
+            return new ColumnInfo(names, values);
+        } catch (IOException ignored) {
+            throw new InternalException(
+                    "Failed to read JSON fields from file %s", file);
+        }
+    }
+
     private List<String> splitLine(String line, String delimiter) {
         if (line == null) {
             return Collections.emptyList();
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
index cd23903a4..31f8a3fa7 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
@@ -285,13 +285,13 @@ public class SaaSMetricsController extends BaseController 
{
         private long vertexLabelCount;
 
         @JsonProperty("vertex-count")
-        private long vertexCount;
+        private Long vertexCount;
 
         @JsonProperty("edge-label-count")
         private long edgeLabelCount;
 
         @JsonProperty("edge-count")
-        private long edgeCount;
+        private Long edgeCount;
 
         @JsonProperty("graph-space-count")
         private long graphSpaceCount;
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
index 63d280868..6361aed2c 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
@@ -42,6 +42,7 @@ import org.apache.hugegraph.entity.query.GremlinQuery;
 import org.apache.hugegraph.entity.space.BuiltInEntity;
 import org.apache.hugegraph.exception.ServerException;
 import org.apache.hugegraph.loader.util.JsonUtil;
+import org.apache.hugegraph.options.HubbleOptions;
 import org.apache.hugegraph.service.algorithm.AsyncTaskService;
 import org.apache.hugegraph.service.auth.UserService;
 import org.apache.hugegraph.service.load.LoadTaskService;
@@ -784,11 +785,15 @@ public class GraphsService {
         Long vertexCount = null;
         String statisticDate = HubbleUtil.dateFormatDay(HubbleUtil.nowDate());
         client.assignGraph(graphSpace, graph);
-        GraphMetricsAPI.ElementCount statistic =
-                client.graph().getEVCount(statisticDate);
-        if (statistic == null) {
-            statisticDate = HubbleUtil.dateFormatLastDay();
+        GraphMetricsAPI.ElementCount statistic = null;
+        boolean pdEnabled = this.config != null &&
+                            this.config.get(HubbleOptions.PD_ENABLED);
+        if (!pdEnabled) {
             statistic = client.graph().getEVCount(statisticDate);
+            if (statistic == null) {
+                statisticDate = HubbleUtil.dateFormatLastDay();
+                statistic = client.graph().getEVCount(statisticDate);
+            }
         }
 
         if (statistic != null) {
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
index 12678e378..a07a09bd2 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
@@ -557,6 +557,7 @@ public class LoadTaskService {
         Ex.check(setting.getColumnNames() != null,
                  "Must do file setting firstly");
         source.header(setting.getColumnNames().toArray(new String[]{}));
+        source.hasHeader(setting.isHasHeader());
         // NOTE: format and delimiter must be CSV and "," temporarily
         source.format(FileFormat.valueOf(setting.getFormat()));
         source.delimiter(setting.getDelimiter());
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
index 3b3813fb8..b5b85dca5 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
@@ -72,8 +72,8 @@ public class GraphSpaceService {
     public Map<String, Long> metrics(HugeClient client) {
         long gsCount = 0L;
         long gCount = 0L;
-        long vCount = 0L;
-        long eCount = 0L;
+        Long vCount = 0L;
+        Long eCount = 0L;
         long vlCount = 0L;
         long elCount = 0L;
         long preDayTaskCount = 0L;
@@ -86,8 +86,10 @@ public class GraphSpaceService {
                 Map<String, Object> elVl = elAndVlCount(client, gs);
                 Map<String, Object> task = preDayTaskCount(client, gs);
 
-                vCount += ((Number) ev.get("vertex")).longValue();
-                eCount += ((Number) ev.get("edge")).longValue();
+                vCount = addAvailableCount(vCount,
+                                           (Number) ev.get("vertex"));
+                eCount = addAvailableCount(eCount,
+                                           (Number) ev.get("edge"));
                 vlCount += ((Number) elVl.get("vertexlabel")).longValue();
                 elCount += ((Number) elVl.get("edgelabel")).longValue();
                 preDayTaskCount += ((Number) task.get("task")).longValue();
@@ -199,8 +201,8 @@ public class GraphSpaceService {
      * @return
      */
     Map<String, Object> evCount(HugeClient client, String graphSpace) {
-        long vertexTotal = 0L;
-        long edgeTotal = 0L;
+        Long vertexTotal = 0L;
+        Long edgeTotal = 0L;
         Map<String, Object> statisticTotal = new HashMap<>();
         client.assignGraph(graphSpace, "");
         Set<String> graphs = graphsService.listGraphNames(client, graphSpace, 
"");
@@ -219,8 +221,10 @@ public class GraphSpaceService {
                 statisticDate = null;
             }
 
-            vertexTotal += ((Number) graphEvCount.get("vertex")).longValue();
-            edgeTotal += ((Number) graphEvCount.get("edge")).longValue();
+            Number vertexCount = (Number) graphEvCount.get("vertex");
+            Number edgeCount = (Number) graphEvCount.get("edge");
+            vertexTotal = addAvailableCount(vertexTotal, vertexCount);
+            edgeTotal = addAvailableCount(edgeTotal, edgeCount);
         }
         if (graphs.isEmpty()) {
             statisticDate = HubbleUtil.dateFormatDay(HubbleUtil.nowDate());
@@ -232,6 +236,13 @@ public class GraphSpaceService {
         return statisticTotal;
     }
 
+    private static Long addAvailableCount(Long total, Number count) {
+        if (total == null || count == null) {
+            return null;
+        }
+        return Math.addExact(total, count.longValue());
+    }
+
     /**
      * 统计指定图空间下的edgeLabel总数和vertexLabel边总数
      * @param client
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
index f3e6b7f34..e7258ab26 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
@@ -34,6 +34,7 @@ import com.google.common.collect.ImmutableSet;
 public final class HugeClientUtil {
 
     private static final String DEFAULT_PROTOCOL = "http";
+    private static final String BEARER_SCHEME = "Bearer";
 
     private static final Set<String> ACCEPTABLE_EXCEPTIONS = ImmutableSet.of(
             "Permission denied: execute Resource"
@@ -44,7 +45,12 @@ public final class HugeClientUtil {
         String graph = connection.getGraph();
         String host = connection.getHost();
         Integer port = connection.getPort();
-        String token = normalizeToken(connection.getToken());
+        String tokenInput = connection.getToken();
+        String token = normalizeTokenPayload(tokenInput);
+        String authContext = null;
+        if (StringUtils.isNotBlank(tokenInput)) {
+            authContext = bearerAuthContext(tokenInput);
+        }
         String username = connection.getUsername();
         String password = connection.getPassword();
         int timeout = connection.getTimeout();
@@ -112,14 +118,36 @@ public final class HugeClientUtil {
             throw e;
         }
 
+        if (authContext != null) {
+            client.setAuthContext(authContext);
+        }
+
         return client;
     }
 
-    private static String normalizeToken(String token) {
-        if (StringUtils.isBlank(token) || token.startsWith(" ")) {
-            return token;
+    public static String bearerAuthContext(String token) {
+        String normalized = normalizeTokenPayload(token);
+        if (StringUtils.isBlank(normalized)) {
+            throw new IllegalArgumentException(
+                    "Bearer token must contain a non-blank payload");
+        }
+        return BEARER_SCHEME + " " + normalized;
+    }
+
+    private static String normalizeTokenPayload(String token) {
+        String normalized = StringUtils.stripToNull(token);
+        if (normalized == null) {
+            return null;
+        }
+        int schemeLength = BEARER_SCHEME.length();
+        if (normalized.length() >= schemeLength &&
+            normalized.regionMatches(true, 0, BEARER_SCHEME, 0, schemeLength) 
&&
+            (normalized.length() == schemeLength ||
+             Character.isWhitespace(normalized.charAt(schemeLength)))) {
+            normalized = StringUtils.stripToNull(
+                    normalized.substring(schemeLength));
         }
-        return " " + token;
+        return normalized;
     }
 
     private static boolean isAcceptable(String message) {
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
index 79a03d224..639bc341b 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
@@ -45,6 +45,7 @@ import org.apache.hugegraph.entity.enums.JobStatus;
 import org.apache.hugegraph.entity.enums.LoadStatus;
 import org.apache.hugegraph.entity.GraphConnection;
 import org.apache.hugegraph.entity.load.Datasource;
+import org.apache.hugegraph.entity.load.FileSetting;
 import org.apache.hugegraph.entity.load.FileMapping;
 import org.apache.hugegraph.entity.load.JobManager;
 import org.apache.hugegraph.entity.load.LoadTask;
@@ -123,6 +124,7 @@ public class IngestControllerTest {
                             mapping.getFileStatus());
         Assert.assertEquals(Collections.singletonList("name"),
                             mapping.getFileSetting().getColumnNames());
+        Assert.assertFalse(mapping.getFileSetting().isHasHeader());
         Assert.assertEquals(Collections.singletonList("name"),
                             mapping.getVertexMappings().iterator().next()
                                    .getIdFields());
@@ -130,6 +132,130 @@ public class IngestControllerTest {
                                                         .getJobStatus());
     }
 
+    @Test
+    public void testDatasourceSchemaUsesPhysicalHeaderWhenNamesOmitted()
+           throws Exception {
+        Path uploadRoot = Files.createTempDirectory("hubble-ingest-schema");
+        Path dataFile = uploadRoot.resolve("data.csv");
+        Files.write(dataFile, List.of("name", "Carol"));
+
+        IngestController controller = new IngestController();
+        DatasourceService datasourceService =
+                Mockito.mock(DatasourceService.class);
+        FileMappingService fileMappingService =
+                Mockito.mock(FileMappingService.class);
+        this.setField(controller, "datasourceService", datasourceService);
+        this.setField(controller, "fileMappingService", fileMappingService);
+
+        Datasource datasource = new Datasource();
+        datasource.setId(1);
+        Map<String, Object> datasourceConfig = new HashMap<>();
+        datasourceConfig.put("type", "FILE");
+        datasourceConfig.put("path", dataFile.toString());
+        datasourceConfig.put("format", "CSV");
+        datasource.setDatasourceConfig(datasourceConfig);
+        Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+        Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+                dataFile.toString())).thenReturn(dataFile.toFile());
+
+        Response response = controller.datasourceSchema(1);
+
+        Assert.assertEquals(Constant.STATUS_OK, response.getStatus());
+        Assert.assertEquals(Collections.singletonList("name"),
+                            response.getData());
+    }
+
+    @Test
+    public void testDatasourceSchemaUsesJsonObjectKeys()
+           throws Exception {
+        Path uploadRoot = 
Files.createTempDirectory("hubble-ingest-json-schema");
+        Path dataFile = uploadRoot.resolve("data.json");
+        Files.write(dataFile,
+                    
Collections.singletonList("{\"name\":\"Carol\",\"age\":1}"));
+
+        IngestController controller = new IngestController();
+        DatasourceService datasourceService =
+                Mockito.mock(DatasourceService.class);
+        FileMappingService fileMappingService =
+                Mockito.mock(FileMappingService.class);
+        this.setField(controller, "datasourceService", datasourceService);
+        this.setField(controller, "fileMappingService", fileMappingService);
+
+        Datasource datasource = new Datasource();
+        datasource.setId(1);
+        Map<String, Object> datasourceConfig = new HashMap<>();
+        datasourceConfig.put("type", "FILE");
+        datasourceConfig.put("path", dataFile.toString());
+        datasourceConfig.put("format", "JSON");
+        datasource.setDatasourceConfig(datasourceConfig);
+        Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+        Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+                dataFile.toString())).thenReturn(dataFile.toFile());
+
+        Response response = controller.datasourceSchema(1);
+
+        Assert.assertEquals(Constant.STATUS_OK, response.getStatus());
+        Assert.assertEquals(List.of("name", "age"), response.getData());
+    }
+
+    @Test
+    public void testDatasourceSchemaDoesNotEchoMalformedJson()
+           throws Exception {
+        Path uploadRoot = Files.createTempDirectory("hubble-ingest-bad-json");
+        Path dataFile = uploadRoot.resolve("data.json");
+        String canary = "private-json-canary";
+        Files.write(dataFile, Collections.singletonList(
+                "{\"password\":\"" + canary + "\""));
+
+        IngestController controller = new IngestController();
+        DatasourceService datasourceService =
+                Mockito.mock(DatasourceService.class);
+        FileMappingService fileMappingService =
+                Mockito.mock(FileMappingService.class);
+        this.setField(controller, "datasourceService", datasourceService);
+        this.setField(controller, "fileMappingService", fileMappingService);
+
+        Datasource datasource = new Datasource();
+        datasource.setId(1);
+        Map<String, Object> datasourceConfig = new HashMap<>();
+        datasourceConfig.put("type", "FILE");
+        datasourceConfig.put("path", dataFile.toString());
+        datasourceConfig.put("format", "JSON");
+        datasource.setDatasourceConfig(datasourceConfig);
+        Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+        Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+                dataFile.toString())).thenReturn(dataFile.toFile());
+
+        try {
+            controller.datasourceSchema(1);
+            Assert.fail("Expected malformed JSON to be rejected");
+        } catch (Exception e) {
+            Assert.assertFalse(e.toString().contains(canary));
+        }
+    }
+
+    @Test
+    public void testCreateFileTaskMarksDetectedHeaderAsPhysical()
+           throws Exception {
+        FileSetting setting = this.captureFileSetting(
+                List.of("name", "Carol"), "CSV");
+
+        Assert.assertTrue(setting.isHasHeader());
+        Assert.assertEquals(Collections.singletonList("name"),
+                            setting.getColumnNames());
+    }
+
+    @Test
+    public void testCreateJsonFileTaskDoesNotMarkPhysicalHeader()
+           throws Exception {
+        FileSetting setting = this.captureFileSetting(
+                Collections.singletonList("{\"name\":\"Carol\"}"), "JSON");
+
+        Assert.assertFalse(setting.isHasHeader());
+        Assert.assertEquals(Collections.singletonList("name"),
+                            setting.getColumnNames());
+    }
+
     @Test
     public void testCreateFileTaskRejectsEmptyMappingBeforePersistence()
            throws Exception {
@@ -262,6 +388,53 @@ public class IngestControllerTest {
         return request;
     }
 
+    private FileSetting captureFileSetting(List<String> lines, String format)
+            throws Exception {
+        Path uploadRoot = Files.createTempDirectory("hubble-ingest-capture");
+        Path dataFile = uploadRoot.resolve("data." + format.toLowerCase());
+        Files.write(dataFile, lines);
+
+        TestIngestController controller = new TestIngestController();
+        DatasourceService datasourceService =
+                Mockito.mock(DatasourceService.class);
+        JobManagerService jobService = Mockito.mock(JobManagerService.class);
+        FileMappingService fileMappingService =
+                Mockito.mock(FileMappingService.class);
+        this.setField(controller, "config", this.mockConfig(uploadRoot));
+        this.setField(controller, "datasourceService", datasourceService);
+        this.setField(controller, "jobManagerService", jobService);
+        this.setField(controller, "fileMappingService", fileMappingService);
+
+        Datasource datasource = new Datasource();
+        datasource.setId(1);
+        Map<String, Object> datasourceConfig = new HashMap<>();
+        datasourceConfig.put("type", "FILE");
+        datasourceConfig.put("path", dataFile.toString());
+        datasourceConfig.put("format", format);
+        datasource.setDatasourceConfig(datasourceConfig);
+        Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+        Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+                dataFile.toString())).thenReturn(dataFile.toFile());
+        Mockito.when(jobService.createIngestTask(
+                Mockito.any(JobManager.class), Mockito.any(FileMapping.class),
+                Mockito.any(GraphConnection.class), 
Mockito.any(HugeClient.class)))
+               .thenAnswer(invocation -> {
+                   JobManager job = invocation.getArgument(0);
+                   job.setId(7);
+                   return LoadTask.builder().id(9).build();
+               });
+        this.bindRequestSession("alice");
+
+        controller.createTask(this.request(dataFile));
+
+        ArgumentCaptor<FileMapping> mappingCaptor =
+                ArgumentCaptor.forClass(FileMapping.class);
+        Mockito.verify(jobService).createIngestTask(
+                Mockito.any(JobManager.class), mappingCaptor.capture(),
+                Mockito.any(GraphConnection.class), 
Mockito.any(HugeClient.class));
+        return mappingCaptor.getValue().getFileSetting();
+    }
+
     private HugeConfig mockConfig(Path uploadRoot) {
         HugeConfig config = Mockito.mock(HugeConfig.class);
         Mockito.when(config.get(HubbleOptions.UPLOAD_FILE_LOCATION))
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/saas/SaaSMetricsControllerTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/saas/SaaSMetricsControllerTest.java
new file mode 100644
index 000000000..4fef871ad
--- /dev/null
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/saas/SaaSMetricsControllerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.controller.saas;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.service.saas.PrometheusService;
+import org.apache.hugegraph.service.space.GraphSpaceService;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class SaaSMetricsControllerTest {
+
+    @Test
+    public void testMetricsSerializesUnavailableElementCounts() {
+        HugeClient client = Mockito.mock(HugeClient.class);
+        GraphSpaceService graphSpaceService =
+                Mockito.mock(GraphSpaceService.class);
+        PrometheusService prometheusService =
+                Mockito.mock(PrometheusService.class);
+        SaaSMetricsController controller = new SaaSMetricsController() {
+            @Override
+            protected HugeClient authClient(String graphSpace, String graph) {
+                return client;
+            }
+        };
+        ReflectionTestUtils.setField(controller, "graphSpaceService",
+                                     graphSpaceService);
+        ReflectionTestUtils.setField(controller, "prometheusService",
+                                     prometheusService);
+
+        Map<String, Long> metrics = new HashMap<>();
+        metrics.put("gsCount", 1L);
+        metrics.put("gCount", 2L);
+        metrics.put("vCount", null);
+        metrics.put("eCount", null);
+        metrics.put("vlCount", 3L);
+        metrics.put("elCount", 4L);
+        metrics.put("preDayTaskCount", 5L);
+        Mockito.when(graphSpaceService.metrics(client)).thenReturn(metrics);
+        Mockito.when(prometheusService.queryCountOffSet1Day(
+                             Mockito.anyString())).thenReturn(6L);
+
+        Object result = controller.metrics();
+        JsonNode json = new ObjectMapper().valueToTree(result);
+
+        Assert.assertTrue(json.get("vertex-count").isNull());
+        Assert.assertTrue(json.get("edge-count").isNull());
+        Assert.assertEquals(1L, json.get("graph-space-count").longValue());
+        Assert.assertEquals(2L, json.get("graph-count").longValue());
+    }
+}
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
index 72df7ebae..2f5958124 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
@@ -137,6 +137,26 @@ public class GraphSpaceServiceTest {
         Assert.assertEquals(Long.valueOf(3L), result.get("eCount"));
     }
 
+    @Test
+    public void testMetricsPreservesUnavailableElementCounts() {
+        GraphSpaceService spy = Mockito.spy(this.service);
+        Mockito.doReturn(java.util.Arrays.asList("available", "unavailable"))
+               .when(spy).listAll(this.client);
+        Mockito.doReturn(statistic("20260712", 2L, 3L))
+               .when(spy).evCount(this.client, "available");
+        Mockito.doReturn(statistic(null, null, null))
+               .when(spy).evCount(this.client, "unavailable");
+        Mockito.when(this.graphsService.listGraphNames(
+                             Mockito.eq(this.client), Mockito.anyString(),
+                             Mockito.eq("")))
+               .thenReturn(java.util.Collections.emptySet());
+
+        Map<String, Long> result = spy.metrics(this.client);
+
+        Assert.assertNull(result.get("vCount"));
+        Assert.assertNull(result.get("eCount"));
+    }
+
     @Test
     public void testStatisticDoesNotClaimMixedDates() {
         LinkedHashSet<String> graphs = new LinkedHashSet<>();
@@ -203,6 +223,27 @@ public class GraphSpaceServiceTest {
         Assert.assertEquals(10L, result.get("edge"));
     }
 
+    @Test
+    public void testStatisticKeepsUnavailableCountsForGraphSpace() {
+        LinkedHashSet<String> graphs = new LinkedHashSet<>();
+        graphs.add("available");
+        graphs.add("unavailable");
+        Mockito.when(this.graphsService.listGraphNames(this.client, "space", 
""))
+               .thenReturn(graphs);
+        Mockito.when(this.graphsService.evCount(this.client, "space",
+                                                "available"))
+               .thenReturn(statistic("20260712", 2L, 3L));
+        Mockito.when(this.graphsService.evCount(this.client, "space",
+                                                "unavailable"))
+               .thenReturn(statistic(null, null, null));
+
+        Map<String, Object> result = this.service.evCount(this.client, 
"space");
+
+        Assert.assertNull(result.get("date"));
+        Assert.assertNull(result.get("vertex"));
+        Assert.assertNull(result.get("edge"));
+    }
+
     @Test
     public void testStatisticFormatsCurrentDateForEmptyGraphSpace() {
         Mockito.when(this.graphsService.listGraphNames(this.client, "space", 
""))
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
index 0fcff79d0..1e5a7c6a3 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
@@ -81,6 +81,44 @@ public class BaseControllerGremlinClientTest {
         Assert.assertEquals("hugegraph", controller.graph);
     }
 
+    @Test
+    public void testReusedTemporaryClientUsesBearerToken() {
+        HugeClient tokenClient = Mockito.mock(HugeClient.class);
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.getSession().setAttribute(Constant.USERNAME_KEY, "admin");
+        request.getSession().setAttribute(Constant.TOKEN_KEY, "jwt-token");
+        request.setAttribute("hugeClient", tokenClient);
+        RequestContextHolder.setRequestAttributes(
+                new ServletRequestAttributes(request));
+
+        TestController controller = new TestController();
+
+        Assert.assertSame(tokenClient, controller.temporaryTokenClient());
+        Mockito.verify(tokenClient).setAuthContext("Bearer jwt-token");
+    }
+
+    @Test
+    public void testInvalidTokenIsRejectedForReusedTemporaryClient() {
+        HugeClient tokenClient = Mockito.mock(HugeClient.class);
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.getSession().setAttribute(Constant.USERNAME_KEY, "admin");
+        request.getSession().setAttribute(Constant.TOKEN_KEY, "Bearer   ");
+        request.setAttribute("hugeClient", tokenClient);
+        RequestContextHolder.setRequestAttributes(
+                new ServletRequestAttributes(request));
+
+        TestController controller = new TestController();
+
+        try {
+            controller.temporaryTokenClient();
+            Assert.fail("Expected a scheme-only Bearer token to be rejected");
+        } catch (IllegalArgumentException ignored) {
+            // Expected
+        }
+        Mockito.verify(tokenClient, Mockito.never())
+               .setAuthContext(Mockito.anyString());
+    }
+
     private MockHttpServletRequest requestWithAuth() {
         MockHttpServletRequest request = new MockHttpServletRequest();
         request.getSession().setAttribute(Constant.USERNAME_KEY, "admin");
@@ -102,6 +140,10 @@ public class BaseControllerGremlinClientTest {
             return this.authGremlinClient(graphSpace, graph);
         }
 
+        HugeClient temporaryTokenClient() {
+            return this.tempTokenClient();
+        }
+
         @Override
         protected HugeClient authClient(String graphSpace, String graph) {
             this.authClientCreated = true;
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
index b381e293c..7cd14011a 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
@@ -319,6 +319,36 @@ public class GraphsServiceDefaultTest {
         Mockito.verifyZeroInteractions(query);
     }
 
+    @Test
+    public void testElementCountSkipsUnavailableSnapshotsInPdMode() {
+        HugeConfig config = Mockito.mock(HugeConfig.class);
+        Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        ReflectionTestUtils.setField(this.service, "config", config);
+
+        QueryService query = Mockito.mock(QueryService.class);
+        ReflectionTestUtils.setField(this.service, "queryService", query);
+
+        GraphManager graph = Mockito.mock(GraphManager.class);
+        Mockito.when(this.client.graph()).thenReturn(graph);
+        Vertex vertex = Mockito.mock(Vertex.class);
+        Edge edge = Mockito.mock(Edge.class);
+        Mockito.when(vertex.label()).thenReturn("person");
+        Mockito.when(edge.label()).thenReturn("knows");
+        Mockito.when(graph.iterateVertices(1000))
+               .thenReturn(Collections.nCopies(2, vertex).iterator());
+        Mockito.when(graph.iterateEdges(1000))
+               .thenReturn(Collections.singletonList(edge).iterator());
+
+        Map<String, Object> result =
+                this.service.evCount(this.client, "DEFAULT", "demo");
+
+        Assert.assertEquals(2L, result.get("vertex"));
+        Assert.assertEquals(1L, result.get("edge"));
+        Assert.assertNotNull(result.get("date"));
+        Mockito.verify(graph, Mockito.never()).getEVCount(Mockito.anyString());
+        Mockito.verifyZeroInteractions(query);
+    }
+
     @Test
     public void testElementCountReturnsUnavailableWhenLiveFallbackFails() {
         QueryService query = Mockito.mock(QueryService.class);
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
index 287e31429..68c0ab58e 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
@@ -18,8 +18,8 @@
 
 package org.apache.hugegraph.unit;
 
-import java.lang.reflect.Method;
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -28,10 +28,13 @@ import org.junit.Test;
 import org.apache.hugegraph.entity.GraphConnection;
 import org.apache.hugegraph.entity.enums.LoadStatus;
 import org.apache.hugegraph.entity.load.FileMapping;
+import org.apache.hugegraph.entity.load.FileSetting;
+import org.apache.hugegraph.entity.load.ListFormat;
 import org.apache.hugegraph.entity.load.LoadParameter;
 import org.apache.hugegraph.entity.load.LoadTask;
 import org.apache.hugegraph.handler.LoadTaskExecutor;
 import org.apache.hugegraph.loader.executor.LoadOptions;
+import org.apache.hugegraph.loader.source.file.FileSource;
 import org.apache.hugegraph.service.load.LoadTaskService;
 import org.apache.hugegraph.mapper.load.LoadTaskMapper;
 import org.apache.hugegraph.testutil.Assert;
@@ -207,6 +210,35 @@ public class LoadTaskServiceTest {
         Assert.assertEquals(8080, options.port);
     }
 
+    @Test
+    public void testBuildFileSourcePreservesPhysicalHeaderFlag()
+            throws Exception {
+        LoadTaskService service = new LoadTaskService();
+        Method method = LoadTaskService.class.getDeclaredMethod(
+                "buildFileSource", FileMapping.class);
+        method.setAccessible(true);
+        FileMapping mapping = this.fileMapping();
+        FileSetting setting = FileSetting.builder()
+                                         .columnNames(Arrays.asList("name"))
+                                         .hasHeader(false)
+                                         .format("CSV")
+                                         .delimiter(",")
+                                         .charset("UTF-8")
+                                         .dateFormat("yyyy-MM-dd HH:mm:ss")
+                                         .timeZone("GMT+8")
+                                         .skippedLine("(^#|^//).*")
+                                         .listFormat(new ListFormat())
+                                         .build();
+        mapping.setFileSetting(setting);
+
+        FileSource source = (FileSource) method.invoke(service, mapping);
+
+        Assert.assertFalse(source.hasHeader());
+        setting.setHasHeader(true);
+        source = (FileSource) method.invoke(service, mapping);
+        Assert.assertTrue(source.hasHeader());
+    }
+
     @Test
     public void testLoadOptionsPreferDirectServerOverPdPeers()
            throws Exception {
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/util/HugeClientUtilTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/util/HugeClientUtilTest.java
new file mode 100644
index 000000000..eeac2973d
--- /dev/null
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/util/HugeClientUtilTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hugegraph.api.gremlin.GremlinRequest;
+import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.entity.GraphConnection;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+
+public class HugeClientUtilTest {
+
+    @Test
+    public void testTokenClientSendsBearerToGremlin() throws Exception {
+        this.assertAuthorizationHeader("jwt-value", "Bearer jwt-value");
+    }
+
+    @Test
+    public void testTokenClientDoesNotDuplicateBearerScheme() throws Exception 
{
+        this.assertAuthorizationHeader(" bearer jwt-value ",
+                                       "Bearer jwt-value");
+    }
+
+    @Test
+    public void testUnauthenticatedClientDoesNotSendAuthorization()
+            throws Exception {
+        this.assertAuthorizationHeader(null, null);
+    }
+
+    @Test
+    public void testBearerAuthContextRejectsMissingPayload() {
+        this.assertInvalidBearerToken(null);
+        this.assertInvalidBearerToken("");
+        this.assertInvalidBearerToken("   ");
+        this.assertInvalidBearerToken("Bearer");
+        this.assertInvalidBearerToken(" bearer   ");
+        this.assertInvalidBearerToken("Bearer\u2003");
+    }
+
+    @Test
+    public void testSchemeOnlyTokenIsRejectedBeforeVersionRequest()
+            throws Exception {
+        AtomicInteger requests = new AtomicInteger();
+        HttpServer server = HttpServer.create(new 
InetSocketAddress("127.0.0.1",
+                                                                    0), 0);
+        server.createContext("/versions", exchange -> {
+            requests.incrementAndGet();
+            respond(exchange, "{\"versions\":{\"core\":\"1.8.0\"," +
+                    "\"gremlin\":\"3.7.3\",\"api\":\"0.71\"}}");
+        });
+        server.start();
+
+        GraphConnection connection = this.connection(server, "Bearer\u2003");
+        try {
+            HugeClientUtil.tryConnect(connection);
+            Assert.fail("Expected a scheme-only Bearer token to be rejected");
+        } catch (IllegalArgumentException ignored) {
+            Assert.assertEquals(0, requests.get());
+        } finally {
+            server.stop(0);
+        }
+    }
+
+    private void assertAuthorizationHeader(String token, String expected)
+                                           throws Exception {
+        AtomicReference<String> versionAuthorization = new AtomicReference<>();
+        AtomicReference<String> gremlinAuthorization = new AtomicReference<>();
+        HttpServer server = HttpServer.create(new 
InetSocketAddress("127.0.0.1",
+                                                                    0), 0);
+        server.createContext("/versions", exchange -> {
+            versionAuthorization.set(exchange.getRequestHeaders()
+                                             .getFirst("Authorization"));
+            respond(exchange, "{\"versions\":{\"core\":\"1.8.0\"," +
+                    "\"gremlin\":\"3.7.3\",\"api\":\"0.71\"}}");
+        });
+        server.createContext("/gremlin", exchange -> {
+            gremlinAuthorization.set(exchange.getRequestHeaders()
+                                             .getFirst("Authorization"));
+            respond(exchange, "{\"requestId\":\"1\"," +
+                     "\"status\":{\"message\":\"\",\"code\":200," +
+                     "\"attributes\":{}}," +
+                     "\"result\":{\"data\":[1],\"meta\":{}}}");
+        });
+        server.start();
+
+        GraphConnection connection = this.connection(server, token);
+        try (HugeClient client = HugeClientUtil.tryConnect(connection)) {
+            Assert.assertEquals(expected, versionAuthorization.get());
+            client.gremlin().execute(new GremlinRequest("g.V().count()"));
+            Assert.assertEquals(expected, gremlinAuthorization.get());
+        } finally {
+            server.stop(0);
+        }
+    }
+
+    private GraphConnection connection(HttpServer server, String token) {
+        return GraphConnection.builder()
+                              .graphSpace("DEFAULT")
+                              .graph("hugegraph")
+                              .host("127.0.0.1")
+                              .port(server.getAddress().getPort())
+                              .timeout(5)
+                              .token(token)
+                              .build();
+    }
+
+    private void assertInvalidBearerToken(String token) {
+        try {
+            HugeClientUtil.bearerAuthContext(token);
+            Assert.fail("Expected an invalid Bearer token to be rejected");
+        } catch (IllegalArgumentException ignored) {
+            // Expected
+        }
+    }
+
+    private static void respond(HttpExchange exchange, String response)
+                                throws IOException {
+        byte[] body = response.getBytes(StandardCharsets.UTF_8);
+        try (InputStream input = exchange.getRequestBody()) {
+            input.transferTo(OutputStream.nullOutputStream());
+        }
+        exchange.sendResponseHeaders(200, body.length);
+        try (OutputStream output = exchange.getResponseBody()) {
+            output.write(body);
+        }
+    }
+}
diff --git 
a/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature 
b/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature
index 68333599b..550c866be 100644
--- a/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature
+++ b/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature
@@ -6,4 +6,4 @@ Feature: check hugegraph-hubble server health status
     Then  code:<code> response:<response>
     Examples:
       | scene                      | url            | code | response        |
-      | check server health status | localhost:8088 | 200  | {"status":"UP"} |
+      | check server health status | localhost:8088 | 200  | 
{"status":200,"name":"hugegraph-hubble"} |
diff --git 
a/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py 
b/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py
index c84385383..c5a80334c 100644
--- a/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py
+++ b/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py
@@ -29,8 +29,8 @@ use_step_matcher("re")
 
 @when("scene:(?P<scene>.+) url:(?P<url>.+)")
 def step_impl(context, scene, url):
-    http_url = "http://"; + url + "/actuator/health"
-    context.response = requests.get(http_url)
+    http_url = "http://"; + url + "/about"
+    context.response = requests.get(http_url, timeout=5)
     context.code = context.response.status_code
     context.json = context.response.json()
 
@@ -38,7 +38,10 @@ def step_impl(context, scene, url):
 @then("code:(?P<expect_code>.+) response:(?P<expect_json>.+)")
 def step_impl(context, expect_code, expect_json):
     actual_code = context.code
-    actual_json = context.json
+    actual_json = {
+        "status": context.json["status"],
+        "name": context.json["data"]["name"]
+    }
 
     expect_code = int(expect_code)
     expect_json = json.loads(expect_json)
@@ -47,4 +50,5 @@ def step_impl(context, expect_code, expect_json):
         .is_equal_to(expect_code)
     assert_that(actual_json).described_as(context.response) \
         .is_equal_to(expect_json)
-
+    assert_that(context.json["data"]["version"]) \
+        .described_as(context.response).is_not_empty()
diff --git a/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions 
b/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions
index eb160dc80..b071dca5c 100644
--- a/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions
+++ b/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions
@@ -40,19 +40,36 @@ function wait_for_startup() {
     local timeout_s=$2
     local now_s=$(date '+%s')
     local stop_s=$(( now_s + timeout_s ))
-    local status=0
+    local body=""
+    local request_timeout_s=0
+    local sleep_s=0
 
-    while [[ ${now_s} -le ${stop_s} ]]; do
+    while [[ ${now_s} -lt ${stop_s} ]]; do
         echo -n .
-        status=$(curl -o /dev/null -s -w %{http_code} "${server_url}")
-        if [[ ${status} -eq 200 ]]; then
+        request_timeout_s=$(( stop_s - now_s ))
+        if [[ ${request_timeout_s} -gt 5 ]]; then
+            request_timeout_s=5
+        fi
+        body=$(curl -fsS --connect-timeout 1 \
+                    --max-time "${request_timeout_s}" \
+                    "${server_url}" 2>/dev/null || true)
+        if [[ "${body}" == *'"status":200'* &&
+              "${body}" == *'"name":"hugegraph-hubble"'* ]]; then
             echo "OK"
             return 0
         fi
-        sleep 2
+        now_s=$(date '+%s')
+        if [[ ${now_s} -ge ${stop_s} ]]; then
+            break
+        fi
+        sleep_s=$(( stop_s - now_s ))
+        if [[ ${sleep_s} -gt 2 ]]; then
+            sleep_s=2
+        fi
+        sleep "${sleep_s}"
         now_s=$(date '+%s')
     done
 
-    echo "timed out with http status $status" >&2
+    echo "timed out waiting for HugeGraph Hubble readiness response" >&2
     return 1
 }
diff --git a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh 
b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh
index 8699689f1..6c71becb7 100644
--- a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh
+++ b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh
@@ -103,9 +103,9 @@ echo ${PID} > "${PID_FILE}"
 
 # wait hubble start
 TIMEOUT_S=30
-SERVER_HOST=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
hubble.host)
-SERVER_PORT=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
hubble.port)
-SERVER_URL="http://${SERVER_HOST}:${SERVER_PORT}/actuator/health";
+SERVER_HOST=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
server.host)
+SERVER_PORT=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
server.port)
+SERVER_URL="http://${SERVER_HOST}:${SERVER_PORT}/about";
 
 wait_for_startup "${SERVER_URL}" ${TIMEOUT_S} || {
     cat "${LOG}"
diff --git 
a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties 
b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties
index ffe6f7f72..8dfce484c 100644
--- 
a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties
+++ 
b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties
@@ -15,8 +15,8 @@
 # under the License.
 #
 
-hubble.host=0.0.0.0
-hubble.port=8088
+server.host=localhost
+server.port=8088
 
 gremlin.suffix_limit=250
 gremlin.vertex_degree_limit=100
diff --git 
a/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py 
b/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py
index 0fbf6518e..0f6031a96 100755
--- a/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py
@@ -84,7 +84,7 @@ def is_hubble_readiness_response(response):
         return False
     data = response.get("data")
     return (isinstance(data, dict) and
-            isinstance(data.get("name"), str) and bool(data["name"]) and
+            data.get("name") == "hugegraph-hubble" and
             isinstance(data.get("version"), str) and bool(data["version"]))
 
 
@@ -144,23 +144,15 @@ def configure_hubble_endpoint(hubble_home, hubble_url, 
bind_host, server_url):
     lines = []
     replaced_host = False
     replaced_port = False
-    replaced_server_address = False
-    replaced_server_port = False
     replaced_pd_enabled = False
     replaced_direct_url = False
     for line in text.splitlines():
-        if host and line.startswith("hubble.host="):
-            lines.append(f"hubble.host={host}")
+        if host and line.startswith("server.host="):
+            lines.append(f"server.host={host}")
             replaced_host = True
-        elif port and line.startswith("hubble.port="):
-            lines.append(f"hubble.port={port}")
-            replaced_port = True
-        elif host and line.startswith("server.address="):
-            lines.append(f"server.address={host}")
-            replaced_server_address = True
         elif port and line.startswith("server.port="):
             lines.append(f"server.port={port}")
-            replaced_server_port = True
+            replaced_port = True
         elif line.startswith("pd.enabled="):
             lines.append("pd.enabled=false")
             replaced_pd_enabled = True
@@ -170,12 +162,8 @@ def configure_hubble_endpoint(hubble_home, hubble_url, 
bind_host, server_url):
         else:
             lines.append(line)
     if host and not replaced_host:
-        lines.append(f"hubble.host={host}")
-    if host and not replaced_server_address:
-        lines.append(f"server.address={host}")
+        lines.append(f"server.host={host}")
     if port and not replaced_port:
-        lines.append(f"hubble.port={port}")
-    if port and not replaced_server_port:
         lines.append(f"server.port={port}")
     if not replaced_pd_enabled:
         lines.append("pd.enabled=false")
diff --git 
a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js 
b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
index 7ca54f803..1b46bd1cf 100755
--- a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
@@ -81,6 +81,59 @@ async function loadPlaywright() {
   }
 }
 
+function createApiEntry(response) {
+  const request = response.request();
+  return {
+    method: request.method(),
+    url: response.url(),
+    httpStatus: response.status(),
+    ok: response.ok(),
+    businessStatus: null
+  };
+}
+
+async function readBusinessStatus(response, entry) {
+  try {
+    const body = await response.json();
+    entry.businessStatus = body && body.status !== undefined ? body.status : 
null;
+  } catch (_) {
+    // Non-JSON API responses are still represented by HTTP status.
+  }
+  return entry;
+}
+
+function matchesRequiredApi(response, requiredRequest) {
+  try {
+    const url = new URL(response.url());
+    return response.request().method() === 'GET' &&
+           url.pathname === requiredRequest.path &&
+           Object.entries(requiredRequest.query || {}).every(([key, value]) => 
(
+             url.searchParams.get(key) === value
+           ));
+  } catch (_) {
+    return false;
+  }
+}
+
+async function waitForRequiredResponse(page, requiredRequest) {
+  try {
+    const response = await page.waitForResponse(
+      item => matchesRequiredApi(item, requiredRequest),
+      {timeout: 30000}
+    );
+    return {
+      entry: await readBusinessStatus(response, createApiEntry(response)),
+      error: null
+    };
+  } catch (error) {
+    return {entry: null, error: error.message};
+  }
+}
+
+function apiEntryPassed(entry) {
+  return Boolean(entry && entry.ok && entry.businessStatus === 200);
+}
+
 async function main() {
   const hubbleUrl = (argValue('--hubble-url', process.env.HUBBLE_URL) ||
                      'http://127.0.0.1:8088').replace(/\/$/, '');
@@ -106,23 +159,11 @@ async function main() {
   const auth = await authenticateUi(context, page, hubbleUrl, username, 
password);
 
   page.on('response', async (response) => {
-    const request = response.request();
     const url = response.url();
     if (url.includes('/api/v1.3/')) {
-      let businessStatus = null;
-      try {
-        const body = await response.json();
-        businessStatus = body && body.status !== undefined ? body.status : 
null;
-      } catch (_) {
-        // Non-JSON API responses are still represented by HTTP status.
-      }
-      network.push({
-        method: request.method(),
-        url,
-        httpStatus: response.status(),
-        ok: response.ok(),
-        businessStatus
-      });
+      const entry = createApiEntry(response);
+      network.push(entry);
+      await readBusinessStatus(response, entry);
     }
   });
   page.on('console', (message) => {
@@ -133,21 +174,27 @@ async function main() {
 
   const graphspaceRoute = auth.pdEnabled
     ? { name: 'graphspace', path: '/graphspace',
-        requiredApis: ['/api/v1.3/graphspaces'],
+        requiredRequests: [{
+          path: '/api/v1.3/graphspaces',
+          query: {page_no: '1', page_size: '11'}
+        }],
         readySelector: '[data-testid="graphspace-page-title"]' }
-    : { name: 'graphspace', path: '/graphspace',
-        requiredApis: ['/api/v1.3/graphspaces/DEFAULT/graphs'],
+    : { name: 'graphspace', path: '/graphspace/DEFAULT',
+        requiredRequests: [{
+          path: '/api/v1.3/graphspaces/DEFAULT/graphs',
+          query: {page_no: '1', page_size: '11'}
+        }],
         textPattern: /图管理|Graph Management/ };
   const routes = [
     graphspaceRoute,
     { name: 'gremlin', path: '/gremlin',
-      requiredApis: ['/api/v1.3/graphspaces/list'],
+      requiredRequests: [{path: '/api/v1.3/graphspaces/list'}],
       textPattern: /Gremlin|图查询|查询/ },
     { name: 'algorithms', path: '/algorithms',
-      requiredApis: ['/api/v1.3/graphspaces/list'],
+      requiredRequests: [{path: '/api/v1.3/graphspaces/list'}],
       textPattern: /算法|Algorithm|OLTP|OLAP/ },
     { name: 'asyncTasks', path: '/asyncTasks',
-      requiredApis: ['/api/v1.3/graphspaces/list'],
+      requiredRequests: [{path: '/api/v1.3/graphspaces/list'}],
       textPattern: /异步|Async|Task|任务/ }
   ];
 
@@ -155,10 +202,14 @@ async function main() {
   try {
     for (const route of routes) {
       network.length = 0;
+      const requiredResponsePromises = 
route.requiredRequests.map(requiredRequest => (
+        waitForRequiredResponse(page, requiredRequest)
+      ));
       await page.goto(hubbleUrl + route.path, {
-        waitUntil: 'networkidle',
+        waitUntil: 'domcontentloaded',
         timeout: 30000
       });
+      const requiredResponses = await Promise.all(requiredResponsePromises);
       await page.waitForTimeout(500);
       const screenshot = path.join(outputDir, `${route.name}.png`);
       await page.screenshot({ path: screenshot, fullPage: true });
@@ -167,14 +218,15 @@ async function main() {
         '\\b(addition|analysis|async-tasks|common|home|manage|navigation|' +
         'server-data-import|Topbar)\\.[A-Za-z0-9_.-]+'
       );
-      const matchedApis = route.requiredApis.map((requiredApi) => {
-        const entries = network.filter((entry) => 
entry.url.includes(requiredApi));
+      const matchedApis = route.requiredRequests.map((requiredRequest, index) 
=> {
+        const requiredResponse = requiredResponses[index];
+        const entries = requiredResponse.entry ? [requiredResponse.entry] : [];
         return {
-          requiredApi,
+          requiredApi: requiredRequest.path,
+          requiredQuery: requiredRequest.query || {},
           entries,
-          passed: entries.some((entry) => entry.ok &&
-                                  (entry.businessStatus === null ||
-                                   entry.businessStatus === 200))
+          waitError: requiredResponse.error,
+          passed: entries.some(apiEntryPassed)
         };
       });
       const routeTextMatched = route.readySelector
@@ -217,7 +269,17 @@ async function main() {
   }
 }
 
-main().catch((error) => {
-  console.error(error.message);
-  process.exit(1);
-});
+if (require.main === module) {
+  main().catch((error) => {
+    console.error(error.message);
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  apiEntryPassed,
+  createApiEntry,
+  matchesRequiredApi,
+  readBusinessStatus,
+  waitForRequiredResponse
+};
diff --git 
a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.test.js 
b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.test.js
new file mode 100644
index 000000000..cc76af269
--- /dev/null
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.test.js
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const assert = require('node:assert/strict');
+const test = require('node:test');
+
+const {
+  apiEntryPassed,
+  matchesRequiredApi,
+  waitForRequiredResponse
+} = require('./run_ui_browser_smoke');
+
+function response(url, options = {}) {
+  const method = options.method || 'GET';
+  const httpStatus = options.httpStatus || 200;
+  const body = options.body === undefined ? {status: 200} : options.body;
+  return {
+    request: () => ({method: () => method}),
+    url: () => url,
+    status: () => httpStatus,
+    ok: () => httpStatus >= 200 && httpStatus < 300,
+    json: async () => {
+      if (options.jsonError) {
+        throw new Error(options.jsonError);
+      }
+      return body;
+    }
+  };
+}
+
+test('matches the exact page-level required GET request', () => {
+  const requiredRequest = {
+    path: '/api/v1.3/graphspaces/DEFAULT/graphs',
+    query: {page_no: '1', page_size: '11'}
+  };
+  assert.equal(matchesRequiredApi(
+    response(`http://hubble${requiredRequest.path}?page_no=1&page_size=11`),
+    requiredRequest
+  ), true);
+  assert.equal(matchesRequiredApi(
+    response(`http://hubble${requiredRequest.path}?page_no=1&page_size=-1`),
+    requiredRequest
+  ), false);
+  assert.equal(matchesRequiredApi(
+    response(
+      `http://hubble${requiredRequest.path}?page_no=1&page_size=11`,
+      {method: 'POST'}
+    ), requiredRequest
+  ), false);
+});
+
+test('waits for and validates the required API response body', async () => {
+  const requiredRequest = {
+    path: '/api/v1.3/graphspaces/DEFAULT/graphs',
+    query: {page_no: '1', page_size: '11'}
+  };
+  const apiResponse = response(
+    `http://hubble${requiredRequest.path}?page_no=1&page_size=11`,
+    {
+    body: {status: 200, data: {records: []}}
+    }
+  );
+  const page = {
+    waitForResponse: async (predicate, options) => {
+      assert.deepEqual(options, {timeout: 30000});
+      assert.equal(predicate(apiResponse), true);
+      return apiResponse;
+    }
+  };
+
+  const result = await waitForRequiredResponse(page, requiredRequest);
+  assert.equal(result.error, null);
+  assert.deepEqual(result.entry, {
+    method: 'GET',
+    url: `http://hubble${requiredRequest.path}?page_no=1&page_size=11`,
+    httpStatus: 200,
+    ok: true,
+    businessStatus: 200
+  });
+  assert.equal(apiEntryPassed(result.entry), true);
+});
+
+test('keeps a missing required API fail-closed', async () => {
+  const page = {
+    waitForResponse: async () => {
+      throw new Error('required API timeout');
+    }
+  };
+
+  const result = await waitForRequiredResponse(
+    page, {path: '/api/v1.3/graphspaces/DEFAULT/graphs'}
+  );
+  assert.equal(result.entry, null);
+  assert.equal(result.error, 'required API timeout');
+  assert.equal(apiEntryPassed(result.entry), false);
+});
+
+test('rejects unsuccessful or malformed required API responses', async () => {
+  const failures = [
+    response('http://hubble/api/v1.3/graphspaces', {
+      httpStatus: 500,
+      body: {status: 500}
+    }),
+    response('http://hubble/api/v1.3/graphspaces', {
+      body: {status: 500}
+    }),
+    response('http://hubble/api/v1.3/graphspaces', {
+      body: {data: {records: []}}
+    }),
+    response('http://hubble/api/v1.3/graphspaces', {
+      jsonError: 'invalid JSON'
+    })
+  ];
+
+  for (const apiResponse of failures) {
+    const page = {
+      waitForResponse: async predicate => {
+        assert.equal(predicate(apiResponse), true);
+        return apiResponse;
+      }
+    };
+    const result = await waitForRequiredResponse(
+      page, {path: '/api/v1.3/graphspaces'}
+    );
+    assert.equal(apiEntryPassed(result.entry), false);
+  }
+});
diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh 
b/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh
index d47268d7c..f164cf46f 100644
--- a/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh
@@ -94,9 +94,9 @@ pid=$!
 echo ${pid} > "${PID_FILE}"
 
 timeout_s=30
-server_host=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
hubble.host)
-server_port=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
hubble.port)
-server_url="http://${server_host}:${server_port}/actuator/health";
+server_host=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
server.host)
+server_port=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties 
server.port)
+server_url="http://${server_host}:${server_port}/about";
 
 wait_for_startup "${server_url}" ${timeout_s} || {
     cat "${log}"
diff --git 
a/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py 
b/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py
index 945cc9539..78eb8b957 100644
--- a/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py
@@ -17,6 +17,7 @@
 #
 
 import importlib.util
+import tempfile
 import unittest
 from pathlib import Path
 from unittest import mock
@@ -30,17 +31,48 @@ SPEC.loader.exec_module(SMOKE)
 
 class HubbleReadinessTest(unittest.TestCase):
 
+    def test_configure_endpoint_uses_registered_server_options(self):
+        with tempfile.TemporaryDirectory() as directory:
+            home = Path(directory)
+            conf_dir = home / "conf"
+            conf_dir.mkdir()
+            conf = conf_dir / "hugegraph-hubble.properties"
+            conf.write_text(
+                "server.host=localhost\n"
+                "server.port=8088\n"
+                "pd.enabled=true\n",
+                encoding="utf-8"
+            )
+
+            SMOKE.configure_hubble_endpoint(
+                home,
+                "http://127.0.0.1:19088";,
+                "0.0.0.0",
+                "http://127.0.0.1:18080";
+            )
+
+            configured = conf.read_text(encoding="utf-8")
+            self.assertIn("server.host=0.0.0.0", configured)
+            self.assertIn("server.port=19088", configured)
+            self.assertNotIn("hubble.host=", configured)
+            self.assertNotIn("hubble.port=", configured)
+            self.assertNotIn("server.address=", configured)
+
     def test_readiness_rejects_spa_html(self):
         self.assertFalse(SMOKE.is_hubble_readiness_response(
             '<div id="root"></div>'))
 
     def test_readiness_requires_about_response_shape(self):
         self.assertFalse(SMOKE.is_hubble_readiness_response(
-            {"status": 200, "data": {"name": "HugeGraph-Hubble"}}))
-        self.assertTrue(SMOKE.is_hubble_readiness_response({
+            {"status": 200, "data": {"name": "hugegraph-hubble"}}))
+        self.assertFalse(SMOKE.is_hubble_readiness_response({
             "status": 200,
             "data": {"name": "HugeGraph-Hubble", "version": "3.0.0"}
         }))
+        self.assertTrue(SMOKE.is_hubble_readiness_response({
+            "status": 200,
+            "data": {"name": "hugegraph-hubble", "version": "3.0.0"}
+        }))
 
     @mock.patch.object(SMOKE, "request")
     def test_preflight_uses_strict_about_api(self, request):
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/empty-result-copy-context.test.js 
b/hugegraph-hubble/hubble-fe/src/i18n/empty-result-copy-context.test.js
new file mode 100644
index 000000000..931c78b42
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/i18n/empty-result-copy-context.test.js
@@ -0,0 +1,66 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import enAnalysis from './resources/en-US/modules/analysis.json';
+import zhAnalysis from './resources/zh-CN/modules/analysis.json';
+
+const readSource = relativePath => fs.readFileSync(
+    path.resolve(__dirname, '..', relativePath),
+    'utf8'
+);
+
+describe('empty graph result copy stays scoped to its product flow', () => {
+    it.each([
+        ['English', enAnalysis.analysis],
+        ['Chinese', zhAnalysis.analysis],
+    ])('keeps real %s query and algorithm copy distinct', (name, analysis) => {
+        expect(analysis.query_result.no_graph_result).toMatch(/Table|表格/);
+        expect(analysis.query_result.no_graph_result).toMatch(/JSON/);
+        
expect(analysis.algorithm.result.no_graph_result).not.toMatch(/Table|表格|JSON/);
+    });
+
+    it('uses the algorithm-only key in both algorithm empty-result views', () 
=> {
+        const graphResult = 
readSource('modules/algorithm/GraphResult/Home/index.js');
+        const rankResult = readSource(
+            'modules/algorithm/GraphResult/RankApiView/index.js'
+        );
+
+        expect(graphResult).toContain(
+            "t('analysis.algorithm.result.no_graph_result')"
+        );
+        expect(rankResult).toContain(
+            "t('analysis.algorithm.result.no_graph_result')"
+        );
+    });
+
+    it('keeps the query key in the Gremlin empty-graph view', () => {
+        const queryResult = readSource(
+            'modules/analysis/QueryResult/GraphResult/Home/index.js'
+        );
+
+        expect(queryResult).toContain(
+            "t('analysis.query_result.no_graph_result')"
+        );
+        expect(queryResult).not.toContain(
+            "t('analysis.algorithm.result.no_graph_result')"
+        );
+    });
+});
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json
index cec574354..cea1beb88 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json
@@ -545,7 +545,8 @@
         "similarity_value": "Similarity Value",
         "rank_score": "Rank Score:",
         "category": "Category {{index}}",
-        "no_neighbor_at_degree": "No {{index}}-hop neighbors in this scenario"
+        "no_neighbor_at_degree": "No {{index}}-hop neighbors in this scenario",
+        "no_graph_result": "This algorithm returned no graph result to display"
       },
       "mode": {
         "OLTP": "Interactive exploration",
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json
index 3e018c3ef..f2117218a 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json
@@ -545,7 +545,8 @@
         "similarity_value": "相似度的值",
         "rank_score": "排名得分:",
         "category": "分类{{index}}",
-        "no_neighbor_at_degree": "本场景下没有第{{index}}度邻居"
+        "no_neighbor_at_degree": "本场景下没有第{{index}}度邻居",
+        "no_graph_result": "当前算法没有可显示的图结果"
       },
       "mode": {
         "OLTP": "交互式探索",
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js 
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js
index 94eb6043d..76f0326b0 100644
--- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js
+++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js
@@ -536,7 +536,7 @@ const GraphResult = props => {
                 return (
                     <GraphStatusView
                         status={SUCCESS}
-                        message={t('analysis.query_result.no_graph_result')}
+                        
message={t('analysis.algorithm.result.no_graph_result')}
                     />
                 );
             }
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
 
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
index 20da0bb46..40fa906f8 100644
--- 
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
@@ -35,7 +35,7 @@ const RankApiView = props => {
         return (
             <GraphStatusView
                 status={GRAPH_STATUS.SUCCESS}
-                message={t('analysis.query_result.no_graph_result')}
+                message={t('analysis.algorithm.result.no_graph_result')}
             />
         );
     }
diff --git 
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
 
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
index b80ca0985..0c685bed2 100644
--- 
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
+++ 
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
@@ -49,6 +49,8 @@ public final class LoadOptions implements Cloneable {
     private static final int DEFAULT_MAX_CONNECTIONS = CPUS * 4;
     private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = CPUS * 2;
     private static final int MINIMUM_REQUIRED_ARGS = 3;
+    private static final Set<String> SENSITIVE_PARAMETER_FIELDS =
+            ImmutableSet.of("password", "trustStoreToken", "token", "pdToken");
 
     @Parameter(names = {"-f", "--file"}, required = true, arity = 1,
                validateWith = {FileValidator.class},
@@ -359,7 +361,10 @@ public final class LoadOptions implements Cloneable {
         for (Field field : fields) {
             if (field.isAnnotationPresent(Parameter.class)) {
                 try {
-                    LOG.info("    {}={}", field.getName(), field.get(this));
+                    Object value = SENSITIVE_PARAMETER_FIELDS.contains(
+                                   field.getName()) ? "[REDACTED]" :
+                                   field.get(this);
+                    LOG.info("    {}={}", field.getName(), value);
                 } catch (IllegalAccessException e) {
                     e.printStackTrace();
                 }
diff --git 
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
 
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
index d2e05ab7b..a2f1a0607 100644
--- 
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
+++ 
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
@@ -226,6 +226,7 @@ public class FileLineFetcher extends LineFetcher {
      */
     private boolean checkMatchHeader(String line) {
         if (!this.source().format().needHeader() ||
+            Boolean.FALSE.equals(this.source().hasHeader()) ||
             this.offset() != FIRST_LINE_OFFSET) {
             return false;
         }
diff --git 
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
 
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
index 680fe069a..954e5f77d 100644
--- 
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
+++ 
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
@@ -26,6 +26,7 @@ import org.apache.hugegraph.loader.source.AbstractSource;
 import org.apache.hugegraph.loader.source.SourceType;
 import org.apache.hugegraph.loader.util.DateUtil;
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 import com.google.common.collect.ImmutableList;
@@ -58,6 +59,9 @@ public class FileSource extends AbstractSource {
     // Only works for single files
     @JsonProperty("split_count")
     private int splitCount;
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @JsonProperty("has_header")
+    private Boolean hasHeader;
 
     // Whether header needs to be case-sensitive
     private final boolean headerCaseSensitive;
@@ -66,10 +70,9 @@ public class FileSource extends AbstractSource {
         this(null, new DirFilter(), new FileFilter(), FileFormat.CSV,
              Constants.COMMA_STR, Constants.DATE_FORMAT,
              ImmutableList.of(), Constants.TIME_ZONE,
-             new SkippedLine(), Compression.NONE, 500);
+             new SkippedLine(), Compression.NONE, 500, null);
     }
 
-    @JsonCreator
     public FileSource(@JsonProperty("path") String path,
                       @JsonProperty("dir_filter") DirFilter dirFilter,
                       @JsonProperty("filter") FileFilter filter,
@@ -82,6 +85,25 @@ public class FileSource extends AbstractSource {
                       @JsonProperty("skipped_line") SkippedLine skippedLine,
                       @JsonProperty("compression") Compression compression,
                       @JsonProperty("batch_size") Integer batchSize) {
+        this(path, dirFilter, filter, format, delimiter, dateFormat,
+             extraDateFormats, timeZone, skippedLine, compression, batchSize,
+             null);
+    }
+
+    @JsonCreator
+    public FileSource(@JsonProperty("path") String path,
+                      @JsonProperty("dir_filter") DirFilter dirFilter,
+                      @JsonProperty("filter") FileFilter filter,
+                      @JsonProperty("format") FileFormat format,
+                      @JsonProperty("delimiter") String delimiter,
+                      @JsonProperty("date_format") String dateFormat,
+                      @JsonProperty("extra_date_formats")
+                      List<String> extraDateFormats,
+                      @JsonProperty("time_zone") String timeZone,
+                      @JsonProperty("skipped_line") SkippedLine skippedLine,
+                      @JsonProperty("compression") Compression compression,
+                      @JsonProperty("batch_size") Integer batchSize,
+                      @JsonProperty("has_header") Boolean hasHeader) {
         this.path = path;
         this.dirFilter = dirFilter != null ? dirFilter : new DirFilter();
         this.filter = filter != null ? filter : new FileFilter();
@@ -97,6 +119,7 @@ public class FileSource extends AbstractSource {
         this.skippedLine = skippedLine != null ? skippedLine : new 
SkippedLine();
         this.compression = compression != null ? compression : 
Compression.NONE;
         this.batchSize = batchSize != null ? batchSize : 500;
+        this.hasHeader = hasHeader;
 
         // When input is orc/parquet, header is case-insensitive
         if (Compression.ORC.equals(this.compression()) ||
@@ -225,6 +248,14 @@ public class FileSource extends AbstractSource {
         return this.splitCount;
     }
 
+    public Boolean hasHeader() {
+        return this.hasHeader;
+    }
+
+    public void hasHeader(boolean hasHeader) {
+        this.hasHeader = hasHeader;
+    }
+
     @Override
     public FileSource asFileSource() {
         FileSource source = new FileSource();
@@ -240,6 +271,7 @@ public class FileSource extends AbstractSource {
         source.extraDateFormats = this.extraDateFormats;
         source.skippedLine = this.skippedLine;
         source.compression = this.compression;
+        source.hasHeader = this.hasHeader;
         return source;
     }
 
diff --git 
a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/FileLineFetcherTest.java
 
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/FileLineFetcherTest.java
new file mode 100644
index 000000000..d73ac9e65
--- /dev/null
+++ 
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/FileLineFetcherTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hugegraph.loader.test.unit;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.hugegraph.loader.progress.InputItemProgress;
+import org.apache.hugegraph.loader.reader.Readable;
+import org.apache.hugegraph.loader.reader.file.FileLineFetcher;
+import org.apache.hugegraph.loader.reader.line.Line;
+import org.apache.hugegraph.loader.source.file.FileSource;
+import org.apache.hugegraph.testutil.Assert;
+import org.junit.Test;
+
+public class FileLineFetcherTest {
+
+    @Test
+    public void testHeaderlessFileKeepsFirstRowMatchingColumnName()
+            throws Exception {
+        Path file = Files.createTempFile("headerless-", ".csv");
+        Files.write(file, List.of("name", "Carol"));
+        FileLineFetcher fetcher = this.fetcher(file, false);
+        try {
+            Line first = fetcher.fetch();
+            Line second = fetcher.fetch();
+
+            Assert.assertEquals("name", first.rawLine());
+            Assert.assertEquals("Carol", second.rawLine());
+            Assert.assertNull(fetcher.fetch());
+        } finally {
+            fetcher.closeReader();
+            Files.deleteIfExists(file);
+        }
+    }
+
+    @Test
+    public void testPhysicalHeaderSkipsMatchingFirstRow()
+            throws Exception {
+        Path file = Files.createTempFile("physical-header-", ".csv");
+        Files.write(file, List.of("name", "Carol"));
+        FileLineFetcher fetcher = this.fetcher(file, true);
+        try {
+            Line first = fetcher.fetch();
+
+            Assert.assertEquals("Carol", first.rawLine());
+            Assert.assertNull(fetcher.fetch());
+        } finally {
+            fetcher.closeReader();
+            Files.deleteIfExists(file);
+        }
+    }
+
+    private FileLineFetcher fetcher(Path path, boolean hasHeader) {
+        FileSource source = new FileSource();
+        source.header(new String[]{"name"});
+        source.hasHeader(hasHeader);
+        FileLineFetcher fetcher = new FileLineFetcher(source);
+        fetcher.openReader(new TestReadable(path));
+        return fetcher;
+    }
+
+    private static final class TestReadable implements Readable {
+
+        private final Path path;
+
+        private TestReadable(Path path) {
+            this.path = path;
+        }
+
+        @Override
+        public String name() {
+            return this.path.getFileName().toString();
+        }
+
+        @Override
+        public org.apache.hadoop.fs.Path path() {
+            return new org.apache.hadoop.fs.Path(this.path.toString());
+        }
+
+        @Override
+        public InputStream open() throws IOException {
+            return new FileInputStream(this.path.toFile());
+        }
+
+        @Override
+        public InputItemProgress inputItemProgress() {
+            return null;
+        }
+    }
+}
diff --git 
a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
 
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
index b327f59ef..90f466cab 100644
--- 
a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
+++ 
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
@@ -133,6 +133,33 @@ public class LoadOptionsTest {
         }
     }
 
+    @Test
+    public void testDumpParamsRedactsSensitiveValues() {
+        LoadOptions options = new LoadOptions();
+        options.password = "graph-password";
+        options.trustStoreToken = "trust-store-password";
+        options.token = "graph-jwt";
+        options.pdToken = "pd-token";
+
+        CapturingAppender appender = attachAppender();
+        try {
+            options.dumpParams();
+        } finally {
+            detachAppender(appender);
+        }
+
+        Assert.assertTrue(appender.contains("password=[REDACTED]"));
+        Assert.assertTrue(appender.contains("trustStoreToken=[REDACTED]"));
+        Assert.assertTrue(appender.contains("token=[REDACTED]"));
+        Assert.assertTrue(appender.contains("pdToken=[REDACTED]"));
+        Assert.assertTrue(appender.contains("graph=hugegraph"));
+        Assert.assertTrue(appender.contains("host=localhost"));
+        Assert.assertFalse(appender.contains("graph-password"));
+        Assert.assertFalse(appender.contains("trust-store-password"));
+        Assert.assertFalse(appender.contains("graph-jwt"));
+        Assert.assertFalse(appender.contains("pd-token"));
+    }
+
     private static int readStaticInt(Class<?> type, String name)
                                      throws Exception {
         Field field = type.getDeclaredField(name);

Reply via email to