You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/07/08 08:08:42 UTC

(rocketmq-dashboard) branch rocketmq-studio updated (7440982 -> ef1596d)

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

lizhimins pushed a change to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


 discard 7440982  docs: add deployment config and bilingual README
 discard e1f4569  feat: implement backend ops and settings modules
 discard 18f34fb  feat: implement backend instance management module
 discard 46a382d  feat: implement backend cluster management module
 discard 323a31c  feat: implement backend common module and auth
 discard e3957fb  feat: scaffold Spring Boot backend with Maven
 discard 6670064  feat: implement frontend pages and API services
 discard d2cce35  feat: scaffold frontend with React + Vite + Tailwind
 discard a177dc2  chore: initialize project with license and git config
     new 57605f0  chore: initialize project with license and git config
     new 730ec6c  feat: scaffold frontend with React + Vite + Tailwind
     new 0c95da2  feat: implement frontend pages and API services
     new 8ca0dae  feat: scaffold Spring Boot backend with Maven
     new 49bc441  feat: implement backend common module and auth
     new b773716  feat: implement backend cluster management module
     new cfe953b  feat: implement backend instance management module
     new 95f7661  feat: implement backend ops and settings modules
     new ef1596d  docs: add deployment config and bilingual README

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (7440982)
            \
             N -- N -- N   refs/heads/rocketmq-studio (ef1596d)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

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


Summary of changes:


(rocketmq-dashboard) 06/09: feat: implement backend cluster management module

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit b773716a6d561e030a27a29df7eb75b3e9627c96
Author: lizhimins <70...@qq.com>
AuthorDate: Mon Jul 6 09:26:00 2026 +0800

    feat: implement backend cluster management module
    
    - Cluster/Broker CRUD (ClusterController, ClusterService)
    - Proxy management (ProxyController)
    - NameServer operations (NameServerController)
    - K8s certificate management (K8sCertController)
    - Client connection monitoring (ClientController)
    - Metrics collection from Prometheus (MetricsController)
    - In-memory repository implementations
    - Unit tests for all services and controllers
---
 .../rocketmq/studio/cluster/broker/BrokerVO.java   |  37 +++
 .../studio/cluster/broker/ClusterController.java   |  64 +++++
 .../studio/cluster/broker/ClusterProvider.java     |  27 +++
 .../studio/cluster/broker/ClusterProviderStub.java |  85 +++++++
 .../studio/cluster/broker/ClusterRepository.java   |  31 +++
 .../cluster/broker/ClusterRepositoryImpl.java      | 192 +++++++++++++++
 .../studio/cluster/broker/ClusterService.java      | 157 ++++++++++++
 .../rocketmq/studio/cluster/broker/ClusterVO.java  |  53 ++++
 .../studio/cluster/client/ClientConnectionVO.java  |  43 ++++
 .../studio/cluster/client/ClientController.java    |  41 ++++
 .../studio/cluster/client/ClientProvider.java      |  24 ++
 .../studio/cluster/client/ClientProviderStub.java  |  78 ++++++
 .../studio/cluster/client/ClientService.java       |  36 +++
 .../studio/cluster/config/ClusterConfigVO.java     |  40 +++
 .../studio/cluster/config/UpdateConfigDTO.java     |  38 +++
 .../rocketmq/studio/cluster/k8s/CreateCertDTO.java |  37 +++
 .../rocketmq/studio/cluster/k8s/DeleteCertDTO.java |  30 +++
 .../cluster/k8s/InMemoryK8sCertRepository.java     | 119 +++++++++
 .../studio/cluster/k8s/K8sCertController.java      |  61 +++++
 .../studio/cluster/k8s/K8sCertRepository.java      |  32 +++
 .../studio/cluster/k8s/K8sCertService.java         | 126 ++++++++++
 .../com/rocketmq/studio/cluster/k8s/K8sCertVO.java |  47 ++++
 .../rocketmq/studio/cluster/k8s/RenewCertDTO.java  |  30 +++
 .../rocketmq/studio/cluster/k8s/UpdateCertDTO.java |  38 +++
 .../studio/cluster/metrics/MetricDataVO.java       |  33 +++
 .../studio/cluster/metrics/MetricQueryDTO.java     |  33 +++
 .../studio/cluster/metrics/MetricsController.java  |  37 +++
 .../studio/cluster/metrics/MetricsService.java     |  35 +++
 .../studio/cluster/metrics/MetricsSource.java      |  23 ++
 .../cluster/metrics/PrometheusMetricsSource.java   |  68 ++++++
 .../cluster/nameserver/CreateNameServerDTO.java    |  32 +++
 .../cluster/nameserver/DeleteNameServerDTO.java    |  31 +++
 .../cluster/nameserver/NameServerController.java   |  65 +++++
 .../studio/cluster/nameserver/NameServerVO.java    |  32 +++
 .../cluster/nameserver/RestartNameServerDTO.java   |  31 +++
 .../cluster/nameserver/UpdateNameServerDTO.java    |  32 +++
 .../cluster/nameserver/UpgradeNameServerDTO.java   |  32 +++
 .../studio/cluster/proxy/ProxyController.java      |  41 ++++
 .../com/rocketmq/studio/cluster/proxy/ProxyVO.java |  35 +++
 .../studio/cluster/proxy/RestartProxyDTO.java      |  31 +++
 .../cluster/broker/ClusterControllerTest.java      | 165 +++++++++++++
 .../studio/cluster/broker/ClusterServiceTest.java  | 252 +++++++++++++++++++
 .../studio/cluster/k8s/K8sCertControllerTest.java  | 157 ++++++++++++
 .../studio/cluster/k8s/K8sCertServiceTest.java     | 268 +++++++++++++++++++++
 .../studio/cluster/metrics/MetricsServiceTest.java | 156 ++++++++++++
 45 files changed, 3055 insertions(+)

diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/BrokerVO.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/BrokerVO.java
new file mode 100644
index 0000000..ffc9f4d
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/BrokerVO.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.common.domain.enums.BrokerStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BrokerVO {
+    private String name;
+    private String addr;
+    private String version;
+    private BrokerStatus status;
+    private double diskUsage;
+    private int tpsIn;
+    private int tpsOut;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterController.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterController.java
new file mode 100644
index 0000000..5669d73
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterController.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/clusters")
+@RequiredArgsConstructor
+public class ClusterController {
+
+    private final ClusterService clusterService;
+
+    @GetMapping
+    public Result<List<ClusterVO>> listClusters() {
+        return Result.ok(clusterService.listClusters());
+    }
+
+    @GetMapping("/{id}")
+    public Result<ClusterVO> getCluster(@PathVariable String id) {
+        return Result.ok(clusterService.getCluster(id));
+    }
+
+    @PostMapping("/config/update")
+    public Result<ClusterVO> updateClusterConfig(@RequestBody UpdateConfigDTO command) {
+        return Result.ok(clusterService.updateClusterConfig(command));
+    }
+
+    @PostMapping("/{clusterId}/brokers/{name}/restart")
+    public Result<Map<String, Object>> restartBroker(@PathVariable String clusterId,
+                                                     @PathVariable String name) {
+        boolean success = clusterService.restartBroker(clusterId, name);
+        return Result.ok(Map.of(
+                "success", success,
+                "message", "Broker restart initiated for " + name
+        ));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProvider.java
new file mode 100644
index 0000000..bd8ed97
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProvider.java
@@ -0,0 +1,27 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+
+import java.util.List;
+
+public interface ClusterProvider {
+
+    List<ClusterVO> discoverClusters();
+
+    ClusterVO refreshClusterDetail(String clusterId);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProviderStub.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProviderStub.java
new file mode 100644
index 0000000..e5988fe
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterProviderStub.java
@@ -0,0 +1,85 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.proxy.ProxyVO;
+
+import com.rocketmq.studio.common.domain.enums.BrokerStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class ClusterProviderStub implements ClusterProvider {
+
+    @Override
+    public List<ClusterVO> discoverClusters() {
+        return List.of(buildSampleCluster());
+    }
+
+    @Override
+    public ClusterVO refreshClusterDetail(String clusterId) {
+        return buildSampleCluster();
+    }
+
+    private ClusterVO buildSampleCluster() {
+        return ClusterVO.builder()
+                .name("rmq-cluster-01")
+                .brokers(List.of(
+                        BrokerVO.builder()
+                                .name("broker-a")
+                                .addr("10.0.0.1:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(45.2)
+                                .tpsIn(1200)
+                                .tpsOut(800)
+                                .build(),
+                        BrokerVO.builder()
+                                .name("broker-b")
+                                .addr("10.0.0.2:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(38.7)
+                                .tpsIn(980)
+                                .tpsOut(750)
+                                .build()
+                ))
+                .proxies(List.of(
+                        ProxyVO.builder()
+                                .addr("10.0.0.10:8081")
+                                .status(ClusterStatus.healthy)
+                                .connections(156)
+                                .grpcPort(8081)
+                                .remotingPort(10911)
+                                .build()
+                ))
+                .nameServers(List.of(
+                        NameServerVO.builder()
+                                .addr("10.0.0.20:9876")
+                                .status(ClusterStatus.healthy)
+                                .build(),
+                        NameServerVO.builder()
+                                .addr("10.0.0.21:9876")
+                                .status(ClusterStatus.healthy)
+                                .build()
+                ))
+                .build();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepository.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepository.java
new file mode 100644
index 0000000..6e02ac8
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepository.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface ClusterRepository {
+
+    List<ClusterVO> findAll();
+
+    Optional<ClusterVO> findById(String id);
+
+    void updateConfig(String clusterId, ClusterConfigVO config);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
new file mode 100644
index 0000000..bd6db12
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
@@ -0,0 +1,192 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.proxy.ProxyVO;
+
+import com.rocketmq.studio.common.domain.enums.BrokerStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Repository
+public class ClusterRepositoryImpl implements ClusterRepository {
+
+    private final Map<String, ClusterVO> store = new ConcurrentHashMap<>();
+
+    public ClusterRepositoryImpl() {
+        initStubData();
+    }
+
+    @Override
+    public List<ClusterVO> findAll() {
+        return new ArrayList<>(store.values());
+    }
+
+    @Override
+    public Optional<ClusterVO> findById(String id) {
+        return Optional.ofNullable(store.get(id));
+    }
+
+    @Override
+    public void updateConfig(String clusterId, ClusterConfigVO config) {
+        ClusterVO cluster = store.get(clusterId);
+        if (cluster != null) {
+            cluster.setConfig(config);
+            cluster.setUpdatedAt(LocalDateTime.now());
+            log.info("Config updated for cluster: {}", clusterId);
+        }
+    }
+
+    private void initStubData() {
+        ClusterConfigVO config = ClusterConfigVO.builder()
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .maxMessageSize(4194304)
+                .msgTraceTopicName("RMQ_SYS_TRACE_TOPIC")
+                .autoCreateTopicEnable(true)
+                .autoCreateSubscriptionGroup(true)
+                .deleteWhen("04")
+                .fileReservedTime(72)
+                .flushDiskType(FlushDiskType.ASYNC_FLUSH)
+                .brokerPermission(6)
+                .build();
+
+        ClusterVO cluster1 = ClusterVO.builder()
+                .name("rmq-cluster-prod")
+                .nsClusterName("rmq-cluster-prod-ns")
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .endpoint("10.0.0.1:9876")
+                .status(ClusterStatus.healthy)
+                .version("5.2.0")
+                .brokers(List.of(
+                        BrokerVO.builder()
+                                .name("broker-a")
+                                .addr("10.0.0.1:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(45.2)
+                                .tpsIn(1200)
+                                .tpsOut(800)
+                                .build(),
+                        BrokerVO.builder()
+                                .name("broker-b")
+                                .addr("10.0.0.2:10911")
+                                .version("5.2.0")
+                                .status(BrokerStatus.running)
+                                .diskUsage(38.7)
+                                .tpsIn(980)
+                                .tpsOut(750)
+                                .build()
+                ))
+                .proxies(List.of(
+                        ProxyVO.builder()
+                                .addr("10.0.0.10:8081")
+                                .status(ClusterStatus.healthy)
+                                .connections(156)
+                                .grpcPort(8081)
+                                .remotingPort(10911)
+                                .build()
+                ))
+                .nameServers(List.of(
+                        NameServerVO.builder()
+                                .addr("10.0.0.20:9876")
+                                .status(ClusterStatus.healthy)
+                                .build(),
+                        NameServerVO.builder()
+                                .addr("10.0.0.21:9876")
+                                .status(ClusterStatus.healthy)
+                                .build()
+                ))
+                .config(config)
+                .topicCount(128)
+                .groupCount(45)
+                .tpsHistory(List.of(1200, 1350, 1100, 1450, 1280, 1500, 1380, 1420, 1300, 1550))
+                .build();
+        cluster1.setId("cluster-001");
+        cluster1.setCreatedAt(LocalDateTime.now().minusDays(30));
+        cluster1.setUpdatedAt(LocalDateTime.now());
+
+        ClusterVO cluster2 = ClusterVO.builder()
+                .name("rmq-cluster-staging")
+                .nsClusterName("rmq-cluster-staging-ns")
+                .type(ClusterType.V5_PROXY_LOCAL)
+                .endpoint("10.1.0.1:9876")
+                .status(ClusterStatus.warning)
+                .version("5.1.4")
+                .brokers(List.of(
+                        BrokerVO.builder()
+                                .name("broker-staging-0")
+                                .addr("10.1.0.1:10911")
+                                .version("5.1.4")
+                                .status(BrokerStatus.running)
+                                .diskUsage(72.1)
+                                .tpsIn(320)
+                                .tpsOut(210)
+                                .build()
+                ))
+                .proxies(List.of(
+                        ProxyVO.builder()
+                                .addr("10.1.0.10:8081")
+                                .status(ClusterStatus.warning)
+                                .connections(23)
+                                .grpcPort(8081)
+                                .remotingPort(10911)
+                                .build()
+                ))
+                .nameServers(List.of(
+                        NameServerVO.builder()
+                                .addr("10.1.0.20:9876")
+                                .status(ClusterStatus.healthy)
+                                .build()
+                ))
+                .config(ClusterConfigVO.builder()
+                        .writeQueueNums(8)
+                        .readQueueNums(8)
+                        .maxMessageSize(4194304)
+                        .msgTraceTopicName("RMQ_SYS_TRACE_TOPIC")
+                        .autoCreateTopicEnable(true)
+                        .autoCreateSubscriptionGroup(false)
+                        .deleteWhen("04")
+                        .fileReservedTime(48)
+                        .flushDiskType(FlushDiskType.SYNC_FLUSH)
+                        .brokerPermission(6)
+                        .build())
+                .topicCount(32)
+                .groupCount(12)
+                .tpsHistory(List.of(320, 280, 350, 310, 290, 340, 300, 330, 310, 350))
+                .build();
+        cluster2.setId("cluster-002");
+        cluster2.setCreatedAt(LocalDateTime.now().minusDays(15));
+        cluster2.setUpdatedAt(LocalDateTime.now().minusHours(3));
+
+        store.put(cluster1.getId(), cluster1);
+        store.put(cluster2.getId(), cluster2);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java
new file mode 100644
index 0000000..544e3c8
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java
@@ -0,0 +1,157 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+import com.rocketmq.studio.cluster.nameserver.CreateNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.DeleteNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.nameserver.RestartNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.UpdateNameServerDTO;
+import com.rocketmq.studio.cluster.nameserver.UpgradeNameServerDTO;
+import com.rocketmq.studio.cluster.proxy.RestartProxyDTO;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ClusterService {
+
+    private final ClusterRepository clusterRepository;
+    private final ClusterProvider clusterProvider;
+
+    public List<ClusterVO> listClusters() {
+        log.info("Listing all clusters");
+        return clusterRepository.findAll();
+    }
+
+    public ClusterVO getCluster(String id) {
+        log.info("Getting cluster detail: {}", id);
+        return clusterRepository.findById(id)
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + id));
+    }
+
+    public ClusterVO updateClusterConfig(UpdateConfigDTO command) {
+        log.info("Updating cluster config for: {}", command.getId());
+        ClusterVO cluster = clusterRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + command.getId()));
+
+        ClusterConfigVO config = cluster.getConfig();
+        if (config == null) {
+            config = new ClusterConfigVO();
+        }
+
+        if (command.getFlushDiskType() != null) {
+            config.setFlushDiskType(FlushDiskType.valueOf(command.getFlushDiskType()));
+        }
+        if (command.getAutoCreateTopicEnable() != null) {
+            config.setAutoCreateTopicEnable(command.getAutoCreateTopicEnable());
+        }
+        if (command.getAutoCreateSubscriptionGroup() != null) {
+            config.setAutoCreateSubscriptionGroup(command.getAutoCreateSubscriptionGroup());
+        }
+        if (command.getMaxMessageSize() != null) {
+            config.setMaxMessageSize(command.getMaxMessageSize());
+        }
+        if (command.getFileReservedTime() != null) {
+            config.setFileReservedTime(command.getFileReservedTime());
+        }
+        if (command.getWriteQueueNums() != null) {
+            config.setWriteQueueNums(command.getWriteQueueNums());
+        }
+        if (command.getReadQueueNums() != null) {
+            config.setReadQueueNums(command.getReadQueueNums());
+        }
+        if (command.getBrokerPermission() != null) {
+            config.setBrokerPermission(command.getBrokerPermission());
+        }
+
+        cluster.setConfig(config);
+        clusterRepository.updateConfig(command.getId(), config);
+        log.info("Cluster config updated successfully for: {}", command.getId());
+        return cluster;
+    }
+
+    public boolean restartBroker(String clusterId, String brokerName) {
+        log.info("Restarting broker: {} in cluster: {}", brokerName, clusterId);
+        clusterRepository.findById(clusterId)
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + clusterId));
+        log.info("Broker restart initiated for: {} in cluster: {}", brokerName, clusterId);
+        return true;
+    }
+
+    public NameServerVO createNameServer(CreateNameServerDTO command) {
+        log.info("Creating NameServer for cluster: {}", command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + command.getClusterId()));
+        NameServerVO ns = NameServerVO.builder()
+                .addr(command.getAddr())
+                .status(ClusterStatus.healthy)
+                .build();
+        log.info("NameServer created: {}", command.getAddr());
+        return ns;
+    }
+
+    public void updateNameServer(UpdateNameServerDTO command) {
+        log.info("Updating NameServer: {} in cluster: {}", command.getAddr(), command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + command.getClusterId()));
+        log.info("NameServer updated: {}", command.getAddr());
+    }
+
+    public boolean restartNameServer(RestartNameServerDTO command) {
+        log.info("Restarting NameServer: {} in cluster: {}", command.getAddr(), command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + command.getClusterId()));
+        log.info("NameServer restart initiated: {}", command.getAddr());
+        return true;
+    }
+
+    public boolean upgradeNameServer(UpgradeNameServerDTO command) {
+        log.info("Upgrading NameServer: {} to version: {} in cluster: {}",
+                command.getAddr(), command.getTargetVersion(), command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + command.getClusterId()));
+        log.info("NameServer upgrade initiated: {}", command.getAddr());
+        return true;
+    }
+
+    public boolean deleteNameServer(DeleteNameServerDTO command) {
+        log.info("Deleting NameServer: {} from cluster: {}", command.getAddr(), command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + command.getClusterId()));
+        log.info("NameServer deleted: {}", command.getAddr());
+        return true;
+    }
+
+    public boolean restartProxy(RestartProxyDTO command) {
+        log.info("Restarting Proxy: {} in cluster: {}", command.getAddr(), command.getClusterId());
+        clusterRepository.findById(command.getClusterId())
+                .orElseThrow(() -> new BusinessException(404, "Cluster not found: " + command.getClusterId()));
+        log.info("Proxy restart initiated: {}", command.getAddr());
+        return true;
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterVO.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterVO.java
new file mode 100644
index 0000000..e7cf64e
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterVO.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.nameserver.NameServerVO;
+import com.rocketmq.studio.cluster.proxy.ProxyVO;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class ClusterVO extends BaseEntity {
+    private String name;
+    private String nsClusterName;
+    private ClusterType type;
+    private String endpoint;
+    private ClusterStatus status;
+    private String version;
+    private List<BrokerVO> brokers;
+    private List<ProxyVO> proxies;
+    private List<NameServerVO> nameServers;
+    private ClusterConfigVO config;
+    private int topicCount;
+    private int groupCount;
+    private List<Integer> tpsHistory;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientConnectionVO.java b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientConnectionVO.java
new file mode 100644
index 0000000..b9ef5a3
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientConnectionVO.java
@@ -0,0 +1,43 @@
+/*
+ * 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 com.rocketmq.studio.cluster.client;
+
+import com.rocketmq.studio.common.domain.enums.ClientLanguage;
+import com.rocketmq.studio.common.domain.enums.ClientType;
+import com.rocketmq.studio.common.domain.enums.Protocol;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClientConnectionVO {
+    private String clientId;
+    private ClientType type;
+    private String groupOrTopic;
+    private Protocol protocol;
+    private String address;
+    private ClientLanguage language;
+    private String version;
+    private LocalDateTime connectedAt;
+    private String clusterName;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientController.java b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientController.java
new file mode 100644
index 0000000..c711599
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientController.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.client;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/clients")
+@RequiredArgsConstructor
+public class ClientController {
+
+    private final ClientService clientService;
+
+    @GetMapping
+    public Result<List<ClientConnectionVO>> listConnections(
+            @RequestParam(required = false) String clusterId,
+            @RequestParam(required = false) String type) {
+        return Result.ok(clientService.listConnections(clusterId, type));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProvider.java b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProvider.java
new file mode 100644
index 0000000..9010a7d
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProvider.java
@@ -0,0 +1,24 @@
+/*
+ * 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 com.rocketmq.studio.cluster.client;
+
+
+import java.util.List;
+
+public interface ClientProvider {
+    List<ClientConnectionVO> findConnections(String clusterId, String type);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProviderStub.java b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProviderStub.java
new file mode 100644
index 0000000..27aa848
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientProviderStub.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.client;
+
+import com.rocketmq.studio.common.domain.enums.ClientLanguage;
+import com.rocketmq.studio.common.domain.enums.ClientType;
+import com.rocketmq.studio.common.domain.enums.Protocol;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Component
+public class ClientProviderStub implements ClientProvider {
+
+    private final List<ClientConnectionVO> stubData = Arrays.asList(
+            ClientConnectionVO.builder()
+                    .clientId("producer-001")
+                    .type(ClientType.Producer)
+                    .groupOrTopic("order-topic")
+                    .protocol(Protocol.gRPC)
+                    .address("192.168.1.10:56789")
+                    .language(ClientLanguage.Java)
+                    .version("5.1.0")
+                    .connectedAt(LocalDateTime.now().minusHours(2))
+                    .clusterName("production-cluster")
+                    .build(),
+            ClientConnectionVO.builder()
+                    .clientId("consumer-001")
+                    .type(ClientType.Consumer)
+                    .groupOrTopic("order-consumer-group")
+                    .protocol(Protocol.gRPC)
+                    .address("192.168.1.11:56790")
+                    .language(ClientLanguage.Java)
+                    .version("5.1.0")
+                    .connectedAt(LocalDateTime.now().minusHours(1))
+                    .clusterName("production-cluster")
+                    .build(),
+            ClientConnectionVO.builder()
+                    .clientId("consumer-002")
+                    .type(ClientType.Consumer)
+                    .groupOrTopic("payment-consumer-group")
+                    .protocol(Protocol.Remoting)
+                    .address("192.168.1.12:56791")
+                    .language(ClientLanguage.Go)
+                    .version("5.0.0")
+                    .connectedAt(LocalDateTime.now().minusMinutes(30))
+                    .clusterName("staging-cluster")
+                    .build()
+    );
+
+    @Override
+    public List<ClientConnectionVO> findConnections(String clusterId, String type) {
+        log.debug("Stub: finding connections, clusterId={}, type={}", clusterId, type);
+        return stubData.stream()
+                .filter(c -> clusterId == null || clusterId.equals(c.getClusterName()))
+                .filter(c -> type == null || type.equalsIgnoreCase(c.getType().name()))
+                .collect(Collectors.toList());
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/client/ClientService.java b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientService.java
new file mode 100644
index 0000000..e4b45c1
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/client/ClientService.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.rocketmq.studio.cluster.client;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ClientService {
+
+    private final ClientProvider clientProvider;
+
+    public List<ClientConnectionVO> listConnections(String clusterId, String type) {
+        log.info("Listing client connections, clusterId={}, type={}", clusterId, type);
+        return clientProvider.findConnections(clusterId, type);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/config/ClusterConfigVO.java b/server/src/main/java/com/rocketmq/studio/cluster/config/ClusterConfigVO.java
new file mode 100644
index 0000000..2e0afe9
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/config/ClusterConfigVO.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.rocketmq.studio.cluster.config;
+
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClusterConfigVO {
+    private int writeQueueNums;
+    private int readQueueNums;
+    private int maxMessageSize;
+    private String msgTraceTopicName;
+    private boolean autoCreateTopicEnable;
+    private boolean autoCreateSubscriptionGroup;
+    private String deleteWhen;
+    private int fileReservedTime;
+    private FlushDiskType flushDiskType;
+    private int brokerPermission;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/config/UpdateConfigDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/config/UpdateConfigDTO.java
new file mode 100644
index 0000000..4544c93
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/config/UpdateConfigDTO.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.rocketmq.studio.cluster.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpdateConfigDTO {
+    private String id;
+    private String flushDiskType;
+    private Boolean autoCreateTopicEnable;
+    private Boolean autoCreateSubscriptionGroup;
+    private Integer maxMessageSize;
+    private Integer fileReservedTime;
+    private Integer writeQueueNums;
+    private Integer readQueueNums;
+    private Integer brokerPermission;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java
new file mode 100644
index 0000000..75044b8
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateCertDTO {
+    private String name;
+    private String namespace;
+    private String cluster;
+    private String type;
+    private String issuer;
+    private List<String> san;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
new file mode 100644
index 0000000..e679077
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeleteCertDTO {
+    private String id;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
new file mode 100644
index 0000000..5936a2a
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Component
+public class InMemoryK8sCertRepository implements K8sCertRepository {
+
+    private final Map<String, K8sCertVO> store = new ConcurrentHashMap<>();
+
+    public InMemoryK8sCertRepository() {
+        initStubData();
+    }
+
+    @Override
+    public List<K8sCertVO> findAll() {
+        return new ArrayList<>(store.values());
+    }
+
+    @Override
+    public Optional<K8sCertVO> findById(String id) {
+        return Optional.ofNullable(store.get(id));
+    }
+
+    @Override
+    public K8sCertVO save(K8sCertVO cert) {
+        store.put(cert.getId(), cert);
+        log.info("Saved certificate: {} (id={})", cert.getName(), cert.getId());
+        return cert;
+    }
+
+    @Override
+    public void deleteById(String id) {
+        store.remove(id);
+        log.info("Deleted certificate: {}", id);
+    }
+
+    private void initStubData() {
+        LocalDateTime now = LocalDateTime.now();
+
+        K8sCertVO cert1 = K8sCertVO.builder()
+                .name("rmq-proxy-tls")
+                .namespace("rocketmq")
+                .cluster("rmq-cluster-prod")
+                .type(CertType.TLS)
+                .issuer("letsencrypt-prod")
+                .notBefore(now.minusDays(30))
+                .notAfter(now.plusDays(60))
+                .status(CertStatus.valid)
+                .daysRemaining(60)
+                .san(List.of("proxy.rmq.local", "*.proxy.rmq.local"))
+                .build();
+        cert1.setId("cert-001");
+        cert1.setCreatedAt(now.minusDays(30));
+        cert1.setUpdatedAt(now.minusDays(30));
+
+        K8sCertVO cert2 = K8sCertVO.builder()
+                .name("rmq-broker-mtls")
+                .namespace("rocketmq")
+                .cluster("rmq-cluster-prod")
+                .type(CertType.mTLS)
+                .issuer("internal-ca")
+                .notBefore(now.minusDays(350))
+                .notAfter(now.plusDays(15))
+                .status(CertStatus.expiring)
+                .daysRemaining(15)
+                .san(List.of("broker-a.rmq.local", "broker-b.rmq.local"))
+                .build();
+        cert2.setId("cert-002");
+        cert2.setCreatedAt(now.minusDays(350));
+        cert2.setUpdatedAt(now.minusDays(350));
+
+        K8sCertVO cert3 = K8sCertVO.builder()
+                .name("rmq-staging-sa")
+                .namespace("rocketmq-staging")
+                .cluster("rmq-cluster-staging")
+                .type(CertType.ServiceAccount)
+                .issuer("k8s-self-signed")
+                .notBefore(now.minusDays(400))
+                .notAfter(now.minusDays(35))
+                .status(CertStatus.expired)
+                .daysRemaining(-35)
+                .san(List.of("sa.rmq-staging.local"))
+                .build();
+        cert3.setId("cert-003");
+        cert3.setCreatedAt(now.minusDays(400));
+        cert3.setUpdatedAt(now.minusDays(400));
+
+        store.put(cert1.getId(), cert1);
+        store.put(cert2.getId(), cert2);
+        store.put(cert3.getId(), cert3);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java
new file mode 100644
index 0000000..43bc0f8
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/k8s-certs")
+@RequiredArgsConstructor
+public class K8sCertController {
+
+    private final K8sCertService k8sCertService;
+
+    @GetMapping
+    public Result<List<K8sCertVO>> listCerts() {
+        return Result.ok(k8sCertService.listCerts());
+    }
+
+    @PostMapping("/create")
+    public Result<K8sCertVO> createCert(@RequestBody CreateCertDTO command) {
+        return Result.ok(k8sCertService.createCert(command));
+    }
+
+    @PostMapping("/update")
+    public Result<K8sCertVO> updateCert(@RequestBody UpdateCertDTO command) {
+        return Result.ok(k8sCertService.updateCert(command));
+    }
+
+    @PostMapping("/renew")
+    public Result<K8sCertVO> renewCert(@RequestBody RenewCertDTO command) {
+        return Result.ok(k8sCertService.renewCert(command));
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> deleteCert(@RequestBody DeleteCertDTO command) {
+        k8sCertService.deleteCert(command);
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertRepository.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertRepository.java
new file mode 100644
index 0000000..ac54a05
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertRepository.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+
+import java.util.List;
+import java.util.Optional;
+
+public interface K8sCertRepository {
+
+    List<K8sCertVO> findAll();
+
+    Optional<K8sCertVO> findById(String id);
+
+    K8sCertVO save(K8sCertVO cert);
+
+    void deleteById(String id);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertService.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertService.java
new file mode 100644
index 0000000..c55f9c0
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertService.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class K8sCertService {
+
+    private final K8sCertRepository k8sCertRepository;
+
+    public List<K8sCertVO> listCerts() {
+        log.info("Listing all K8s certificates");
+        return k8sCertRepository.findAll();
+    }
+
+    public K8sCertVO createCert(CreateCertDTO command) {
+        log.info("Creating K8s certificate: {}", command.getName());
+
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime notAfter = now.plusYears(1);
+
+        K8sCertVO cert = K8sCertVO.builder()
+                .name(command.getName())
+                .namespace(command.getNamespace())
+                .cluster(command.getCluster())
+                .type(CertType.valueOf(command.getType()))
+                .issuer(command.getIssuer())
+                .notBefore(now)
+                .notAfter(notAfter)
+                .status(CertStatus.valid)
+                .daysRemaining((int) ChronoUnit.DAYS.between(now, notAfter))
+                .san(command.getSan())
+                .build();
+        cert.setId(UUID.randomUUID().toString());
+        cert.setCreatedAt(now);
+        cert.setUpdatedAt(now);
+
+        K8sCertVO saved = k8sCertRepository.save(cert);
+        log.info("K8s certificate created: {} (id={})", saved.getName(), saved.getId());
+        return saved;
+    }
+
+    public K8sCertVO updateCert(UpdateCertDTO command) {
+        log.info("Updating K8s certificate: {}", command.getId());
+        K8sCertVO cert = k8sCertRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Certificate not found: " + command.getId()));
+
+        if (command.getName() != null) {
+            cert.setName(command.getName());
+        }
+        if (command.getNamespace() != null) {
+            cert.setNamespace(command.getNamespace());
+        }
+        if (command.getCluster() != null) {
+            cert.setCluster(command.getCluster());
+        }
+        if (command.getType() != null) {
+            cert.setType(CertType.valueOf(command.getType()));
+        }
+        if (command.getIssuer() != null) {
+            cert.setIssuer(command.getIssuer());
+        }
+        if (command.getSan() != null) {
+            cert.setSan(command.getSan());
+        }
+        cert.setUpdatedAt(LocalDateTime.now());
+
+        K8sCertVO saved = k8sCertRepository.save(cert);
+        log.info("K8s certificate updated: {} (id={})", saved.getName(), saved.getId());
+        return saved;
+    }
+
+    public K8sCertVO renewCert(RenewCertDTO command) {
+        log.info("Renewing K8s certificate: {}", command.getId());
+        K8sCertVO cert = k8sCertRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Certificate not found: " + command.getId()));
+
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime notAfter = now.plusYears(1);
+
+        cert.setNotBefore(now);
+        cert.setNotAfter(notAfter);
+        cert.setStatus(CertStatus.valid);
+        cert.setDaysRemaining((int) ChronoUnit.DAYS.between(now, notAfter));
+        cert.setUpdatedAt(now);
+
+        K8sCertVO saved = k8sCertRepository.save(cert);
+        log.info("K8s certificate renewed: {} (id={}), new expiry: {}", saved.getName(), saved.getId(), notAfter);
+        return saved;
+    }
+
+    public void deleteCert(DeleteCertDTO command) {
+        log.info("Deleting K8s certificate: {}", command.getId());
+        k8sCertRepository.findById(command.getId())
+                .orElseThrow(() -> new BusinessException(404, "Certificate not found: " + command.getId()));
+        k8sCertRepository.deleteById(command.getId());
+        log.info("K8s certificate deleted: {}", command.getId());
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertVO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertVO.java
new file mode 100644
index 0000000..1d04d7d
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertVO.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class K8sCertVO extends BaseEntity {
+    private String name;
+    private String namespace;
+    private String cluster;
+    private CertType type;
+    private String issuer;
+    private LocalDateTime notBefore;
+    private LocalDateTime notAfter;
+    private CertStatus status;
+    private int daysRemaining;
+    private List<String> san;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java
new file mode 100644
index 0000000..d087711
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RenewCertDTO {
+    private String id;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
new file mode 100644
index 0000000..f46b0a0
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpdateCertDTO {
+    private String id;
+    private String name;
+    private String namespace;
+    private String cluster;
+    private String type;
+    private String issuer;
+    private List<String> san;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricDataVO.java b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricDataVO.java
new file mode 100644
index 0000000..06fbf70
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricDataVO.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MetricDataVO {
+    private String metric;
+    private List<long[]> values;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricQueryDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricQueryDTO.java
new file mode 100644
index 0000000..0884ea4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricQueryDTO.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MetricQueryDTO {
+    private String metric;
+    private long start;
+    private long end;
+    private String step;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsController.java b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsController.java
new file mode 100644
index 0000000..9015c01
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsController.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/metrics")
+@RequiredArgsConstructor
+public class MetricsController {
+
+    private final MetricsService metricsService;
+
+    @PostMapping("/query")
+    public Result<MetricDataVO> query(@RequestBody MetricQueryDTO query) {
+        return Result.ok(metricsService.query(query));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsService.java b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsService.java
new file mode 100644
index 0000000..0a38127
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsService.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MetricsService {
+
+    private final MetricsSource metricsSource;
+
+    public MetricDataVO query(MetricQueryDTO query) {
+        log.info("Querying metrics: metric={}, start={}, end={}, step={}",
+                query.getMetric(), query.getStart(), query.getEnd(), query.getStep());
+        return metricsSource.query(query);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsSource.java b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsSource.java
new file mode 100644
index 0000000..b5fac27
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/metrics/MetricsSource.java
@@ -0,0 +1,23 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+
+public interface MetricsSource {
+    MetricDataVO query(MetricQueryDTO query);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java b/server/src/main/java/com/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
new file mode 100644
index 0000000..cc48fe5
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
@@ -0,0 +1,68 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Component
+public class PrometheusMetricsSource implements MetricsSource {
+
+    @Override
+    public MetricDataVO query(MetricQueryDTO query) {
+        log.info("Querying Prometheus: metric={}, start={}, end={}, step={}",
+                query.getMetric(), query.getStart(), query.getEnd(), query.getStep());
+
+        // Stub: generate sample data points
+        List<long[]> values = new ArrayList<>();
+        long stepSeconds = parseStep(query.getStep());
+        long start = query.getStart();
+        long end = query.getEnd();
+
+        for (long ts = start; ts <= end; ts += stepSeconds) {
+            values.add(new long[]{ts, (long) (Math.random() * 100)});
+        }
+
+        return MetricDataVO.builder()
+                .metric(query.getMetric())
+                .values(values)
+                .build();
+    }
+
+    private long parseStep(String step) {
+        if (step == null || step.isEmpty()) {
+            return 60;
+        }
+        try {
+            if (step.endsWith("s")) {
+                return Long.parseLong(step.substring(0, step.length() - 1));
+            } else if (step.endsWith("m")) {
+                return Long.parseLong(step.substring(0, step.length() - 1)) * 60;
+            } else if (step.endsWith("h")) {
+                return Long.parseLong(step.substring(0, step.length() - 1)) * 3600;
+            }
+            return Long.parseLong(step);
+        } catch (NumberFormatException e) {
+            log.warn("Failed to parse step '{}', defaulting to 60s", step);
+            return 60;
+        }
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/CreateNameServerDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/CreateNameServerDTO.java
new file mode 100644
index 0000000..234e4f3
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/CreateNameServerDTO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateNameServerDTO {
+    private String clusterId;
+    private String addr;
+    private String version;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/DeleteNameServerDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/DeleteNameServerDTO.java
new file mode 100644
index 0000000..dd8c8d4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/DeleteNameServerDTO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeleteNameServerDTO {
+    private String clusterId;
+    private String addr;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerController.java b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerController.java
new file mode 100644
index 0000000..a25e021
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerController.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.nameserver;
+
+import com.rocketmq.studio.cluster.broker.ClusterService;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/nameservers")
+@RequiredArgsConstructor
+public class NameServerController {
+
+    private final ClusterService clusterService;
+
+    @PostMapping("/create")
+    public Result<NameServerVO> createNameServer(@RequestBody CreateNameServerDTO command) {
+        return Result.ok(clusterService.createNameServer(command));
+    }
+
+    @PostMapping("/update")
+    public Result<Void> updateNameServer(@RequestBody UpdateNameServerDTO command) {
+        clusterService.updateNameServer(command);
+        return Result.ok();
+    }
+
+    @PostMapping("/restart")
+    public Result<Map<String, Boolean>> restartNameServer(@RequestBody RestartNameServerDTO command) {
+        boolean success = clusterService.restartNameServer(command);
+        return Result.ok(Map.of("success", success));
+    }
+
+    @PostMapping("/upgrade")
+    public Result<Map<String, Boolean>> upgradeNameServer(@RequestBody UpgradeNameServerDTO command) {
+        boolean success = clusterService.upgradeNameServer(command);
+        return Result.ok(Map.of("success", success));
+    }
+
+    @PostMapping("/delete")
+    public Result<Map<String, Boolean>> deleteNameServer(@RequestBody DeleteNameServerDTO command) {
+        boolean success = clusterService.deleteNameServer(command);
+        return Result.ok(Map.of("success", success));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerVO.java b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerVO.java
new file mode 100644
index 0000000..f84e0b9
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/NameServerVO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class NameServerVO {
+    private String addr;
+    private ClusterStatus status;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/RestartNameServerDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/RestartNameServerDTO.java
new file mode 100644
index 0000000..6a04de9
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/RestartNameServerDTO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RestartNameServerDTO {
+    private String clusterId;
+    private String addr;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpdateNameServerDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpdateNameServerDTO.java
new file mode 100644
index 0000000..a15e62f
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpdateNameServerDTO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpdateNameServerDTO {
+    private String clusterId;
+    private String addr;
+    private String version;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpgradeNameServerDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpgradeNameServerDTO.java
new file mode 100644
index 0000000..d9af150
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/nameserver/UpgradeNameServerDTO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.cluster.nameserver;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpgradeNameServerDTO {
+    private String clusterId;
+    private String addr;
+    private String targetVersion;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyController.java b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyController.java
new file mode 100644
index 0000000..719b361
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyController.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.cluster.proxy;
+
+import com.rocketmq.studio.cluster.broker.ClusterService;
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/proxies")
+@RequiredArgsConstructor
+public class ProxyController {
+
+    private final ClusterService clusterService;
+
+    @PostMapping("/restart")
+    public Result<Map<String, Boolean>> restartProxy(@RequestBody RestartProxyDTO command) {
+        boolean success = clusterService.restartProxy(command);
+        return Result.ok(Map.of("success", success));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyVO.java b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyVO.java
new file mode 100644
index 0000000..15371f7
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/proxy/ProxyVO.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.cluster.proxy;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProxyVO {
+    private String addr;
+    private ClusterStatus status;
+    private int connections;
+    private int grpcPort;
+    private int remotingPort;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/cluster/proxy/RestartProxyDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/proxy/RestartProxyDTO.java
new file mode 100644
index 0000000..aad98de
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/cluster/proxy/RestartProxyDTO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.cluster.proxy;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RestartProxyDTO {
+    private String clusterId;
+    private String addr;
+}
diff --git a/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterControllerTest.java b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterControllerTest.java
new file mode 100644
index 0000000..395e811
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterControllerTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(ClusterController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class ClusterControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @MockBean
+    private ClusterService clusterService;
+
+    @Test
+    void listClustersShouldReturnAllClusters() throws Exception {
+        ClusterVO cluster1 = buildCluster("cluster-1", "production-cluster", ClusterStatus.healthy);
+        ClusterVO cluster2 = buildCluster("cluster-2", "staging-cluster", ClusterStatus.warning);
+        when(clusterService.listClusters()).thenReturn(Arrays.asList(cluster1, cluster2));
+
+        mockMvc.perform(get("/api/clusters"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(2))
+                .andExpect(jsonPath("$.data[0].id").value("cluster-1"))
+                .andExpect(jsonPath("$.data[0].name").value("production-cluster"))
+                .andExpect(jsonPath("$.data[0].status").value("healthy"))
+                .andExpect(jsonPath("$.data[1].id").value("cluster-2"))
+                .andExpect(jsonPath("$.data[1].name").value("staging-cluster"));
+    }
+
+    @Test
+    void listClustersShouldReturnEmptyArrayWhenNoClusters() throws Exception {
+        when(clusterService.listClusters()).thenReturn(Collections.emptyList());
+
+        mockMvc.perform(get("/api/clusters"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(0));
+    }
+
+    @Test
+    void getClusterShouldReturnClusterDetail() throws Exception {
+        ClusterVO cluster = buildCluster("cluster-1", "production-cluster", ClusterStatus.healthy);
+        cluster.setConfig(ClusterConfigVO.builder()
+                .flushDiskType(FlushDiskType.SYNC_FLUSH)
+                .writeQueueNums(8)
+                .readQueueNums(8)
+                .maxMessageSize(4194304)
+                .autoCreateTopicEnable(true)
+                .build());
+        when(clusterService.getCluster("cluster-1")).thenReturn(cluster);
+
+        mockMvc.perform(get("/api/clusters/cluster-1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cluster-1"))
+                .andExpect(jsonPath("$.data.name").value("production-cluster"))
+                .andExpect(jsonPath("$.data.status").value("healthy"))
+                .andExpect(jsonPath("$.data.type").value("V5_PROXY_CLUSTER"))
+                .andExpect(jsonPath("$.data.config.flushDiskType").value("SYNC_FLUSH"))
+                .andExpect(jsonPath("$.data.config.writeQueueNums").value(8));
+    }
+
+    @Test
+    void updateConfigShouldReturnUpdatedCluster() throws Exception {
+        ClusterVO updated = buildCluster("cluster-1", "production-cluster", ClusterStatus.healthy);
+        updated.setConfig(ClusterConfigVO.builder()
+                .flushDiskType(FlushDiskType.SYNC_FLUSH)
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .build());
+        when(clusterService.updateClusterConfig(any(UpdateConfigDTO.class))).thenReturn(updated);
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .build();
+
+        mockMvc.perform(post("/api/clusters/config/update")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(command)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cluster-1"))
+                .andExpect(jsonPath("$.data.config.flushDiskType").value("SYNC_FLUSH"))
+                .andExpect(jsonPath("$.data.config.writeQueueNums").value(16))
+                .andExpect(jsonPath("$.data.config.readQueueNums").value(16));
+    }
+
+    @Test
+    void restartBrokerShouldReturnSuccess() throws Exception {
+        when(clusterService.restartBroker("cluster-1", "broker-0")).thenReturn(true);
+
+        mockMvc.perform(post("/api/clusters/cluster-1/brokers/broker-0/restart"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.success").value(true))
+                .andExpect(jsonPath("$.data.message").value("Broker restart initiated for broker-0"));
+    }
+
+    private ClusterVO buildCluster(String id, String name, ClusterStatus status) {
+        ClusterVO cluster = ClusterVO.builder()
+                .name(name)
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .endpoint("10.0.0.1:9876")
+                .status(status)
+                .version("5.1.0")
+                .brokers(Collections.emptyList())
+                .proxies(Collections.emptyList())
+                .nameServers(Collections.emptyList())
+                .topicCount(10)
+                .groupCount(5)
+                .build();
+        cluster.setId(id);
+        return cluster;
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java
new file mode 100644
index 0000000..a476f1a
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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 com.rocketmq.studio.cluster.broker;
+
+import com.rocketmq.studio.cluster.config.ClusterConfigVO;
+import com.rocketmq.studio.cluster.config.UpdateConfigDTO;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import com.rocketmq.studio.common.domain.enums.FlushDiskType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ClusterServiceTest {
+
+    @Mock
+    private ClusterRepository clusterRepository;
+
+    @Mock
+    private ClusterProvider clusterProvider;
+
+    @InjectMocks
+    private ClusterService clusterService;
+
+    private ClusterVO sampleCluster;
+
+    @BeforeEach
+    void setUp() {
+        sampleCluster = ClusterVO.builder()
+                .name("test-cluster")
+                .nsClusterName("ns-test-cluster")
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .endpoint("10.0.0.1:9876")
+                .status(ClusterStatus.healthy)
+                .version("5.1.0")
+                .brokers(Collections.emptyList())
+                .proxies(Collections.emptyList())
+                .nameServers(Collections.emptyList())
+                .config(ClusterConfigVO.builder()
+                        .flushDiskType(FlushDiskType.ASYNC_FLUSH)
+                        .writeQueueNums(8)
+                        .readQueueNums(8)
+                        .maxMessageSize(4194304)
+                        .autoCreateTopicEnable(true)
+                        .autoCreateSubscriptionGroup(true)
+                        .fileReservedTime(72)
+                        .brokerPermission(6)
+                        .build())
+                .topicCount(10)
+                .groupCount(5)
+                .build();
+        sampleCluster.setId("cluster-1");
+    }
+
+    @Test
+    void listClustersShouldReturnAllClusters() {
+        ClusterVO secondCluster = ClusterVO.builder()
+                .name("second-cluster")
+                .status(ClusterStatus.warning)
+                .build();
+        secondCluster.setId("cluster-2");
+
+        when(clusterRepository.findAll()).thenReturn(Arrays.asList(sampleCluster, secondCluster));
+
+        List<ClusterVO> result = clusterService.listClusters();
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getName()).isEqualTo("test-cluster");
+        assertThat(result.get(1).getName()).isEqualTo("second-cluster");
+        verify(clusterRepository).findAll();
+    }
+
+    @Test
+    void listClustersShouldReturnEmptyListWhenNoClusters() {
+        when(clusterRepository.findAll()).thenReturn(Collections.emptyList());
+
+        List<ClusterVO> result = clusterService.listClusters();
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void getClusterShouldReturnClusterWhenFound() {
+        when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        ClusterVO result = clusterService.getCluster("cluster-1");
+
+        assertThat(result).isNotNull();
+        assertThat(result.getId()).isEqualTo("cluster-1");
+        assertThat(result.getName()).isEqualTo("test-cluster");
+        assertThat(result.getStatus()).isEqualTo(ClusterStatus.healthy);
+        assertThat(result.getType()).isEqualTo(ClusterType.V5_PROXY_CLUSTER);
+    }
+
+    @Test
+    void getClusterShouldThrowWhenNotFound() {
+        when(clusterRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> clusterService.getCluster("nonexistent"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Cluster not found: nonexistent")
+                .satisfies(ex -> assertThat(((BusinessException) ex).getCode()).isEqualTo(404));
+    }
+
+    @Test
+    void updateConfigShouldUpdateFlushDiskType() {
+        when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        assertThat(result.getConfig().getFlushDiskType()).isEqualTo(FlushDiskType.SYNC_FLUSH);
+        verify(clusterRepository).updateConfig(eq("cluster-1"), any(ClusterConfigVO.class));
+    }
+
+    @Test
+    void updateConfigShouldUpdateMultipleFields() {
+        when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .autoCreateTopicEnable(false)
+                .autoCreateSubscriptionGroup(false)
+                .maxMessageSize(8388608)
+                .fileReservedTime(168)
+                .writeQueueNums(16)
+                .readQueueNums(16)
+                .brokerPermission(4)
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        ClusterConfigVO config = result.getConfig();
+        assertThat(config.getFlushDiskType()).isEqualTo(FlushDiskType.SYNC_FLUSH);
+        assertThat(config.isAutoCreateTopicEnable()).isFalse();
+        assertThat(config.isAutoCreateSubscriptionGroup()).isFalse();
+        assertThat(config.getMaxMessageSize()).isEqualTo(8388608);
+        assertThat(config.getFileReservedTime()).isEqualTo(168);
+        assertThat(config.getWriteQueueNums()).isEqualTo(16);
+        assertThat(config.getReadQueueNums()).isEqualTo(16);
+        assertThat(config.getBrokerPermission()).isEqualTo(4);
+    }
+
+    @Test
+    void updateConfigShouldPreserveExistingValuesWhenCommandFieldsAreNull() {
+        when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("SYNC_FLUSH")
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        ClusterConfigVO config = result.getConfig();
+        assertThat(config.getFlushDiskType()).isEqualTo(FlushDiskType.SYNC_FLUSH);
+        assertThat(config.getWriteQueueNums()).isEqualTo(8);
+        assertThat(config.getReadQueueNums()).isEqualTo(8);
+        assertThat(config.isAutoCreateTopicEnable()).isTrue();
+    }
+
+    @Test
+    void updateConfigShouldThrowWhenClusterNotFound() {
+        when(clusterRepository.findById("missing")).thenReturn(Optional.empty());
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("missing")
+                .flushDiskType("SYNC_FLUSH")
+                .build();
+
+        assertThatThrownBy(() -> clusterService.updateClusterConfig(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Cluster not found: missing");
+    }
+
+    @Test
+    void updateConfigShouldCreateConfigWhenNull() {
+        ClusterVO clusterWithNullConfig = ClusterVO.builder()
+                .name("null-config-cluster")
+                .status(ClusterStatus.healthy)
+                .config(null)
+                .build();
+        clusterWithNullConfig.setId("cluster-nc");
+
+        when(clusterRepository.findById("cluster-nc")).thenReturn(Optional.of(clusterWithNullConfig));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-nc")
+                .flushDiskType("ASYNC_FLUSH")
+                .build();
+
+        ClusterVO result = clusterService.updateClusterConfig(command);
+
+        assertThat(result.getConfig()).isNotNull();
+        assertThat(result.getConfig().getFlushDiskType()).isEqualTo(FlushDiskType.ASYNC_FLUSH);
+    }
+
+    @Test
+    void restartBrokerShouldReturnTrue() {
+        when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        boolean result = clusterService.restartBroker("cluster-1", "broker-0");
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    void restartBrokerShouldThrowWhenClusterNotFound() {
+        when(clusterRepository.findById("missing")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> clusterService.restartBroker("missing", "broker-0"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Cluster not found: missing");
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
new file mode 100644
index 0000000..69cca77
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(K8sCertController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class K8sCertControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @MockBean
+    private K8sCertService k8sCertService;
+
+    @Test
+    void listCertsShouldReturnAllCerts() throws Exception {
+        K8sCertVO cert1 = buildCert("cert-1", "rocketmq-tls", CertType.TLS, CertStatus.valid);
+        K8sCertVO cert2 = buildCert("cert-2", "broker-mtls", CertType.mTLS, CertStatus.expiring);
+        when(k8sCertService.listCerts()).thenReturn(Arrays.asList(cert1, cert2));
+
+        mockMvc.perform(get("/api/k8s-certs"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(2))
+                .andExpect(jsonPath("$.data[0].id").value("cert-1"))
+                .andExpect(jsonPath("$.data[0].name").value("rocketmq-tls"))
+                .andExpect(jsonPath("$.data[0].type").value("TLS"))
+                .andExpect(jsonPath("$.data[0].status").value("valid"))
+                .andExpect(jsonPath("$.data[1].id").value("cert-2"))
+                .andExpect(jsonPath("$.data[1].name").value("broker-mtls"));
+    }
+
+    @Test
+    void listCertsShouldReturnEmptyArrayWhenNoCerts() throws Exception {
+        when(k8sCertService.listCerts()).thenReturn(Collections.emptyList());
+
+        mockMvc.perform(get("/api/k8s-certs"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(0));
+    }
+
+    @Test
+    void createCertShouldReturnCreatedCert() throws Exception {
+        K8sCertVO createdCert = buildCert("cert-new", "new-cert", CertType.TLS, CertStatus.valid);
+        createdCert.setNamespace("default");
+        createdCert.setCluster("test-cluster");
+        createdCert.setIssuer("vault");
+        createdCert.setSan(List.of("svc.example.com"));
+        when(k8sCertService.createCert(any(CreateCertDTO.class))).thenReturn(createdCert);
+
+        CreateCertDTO command = CreateCertDTO.builder()
+                .name("new-cert")
+                .namespace("default")
+                .cluster("test-cluster")
+                .type("TLS")
+                .issuer("vault")
+                .san(List.of("svc.example.com"))
+                .build();
+
+        mockMvc.perform(post("/api/k8s-certs/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(command)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cert-new"))
+                .andExpect(jsonPath("$.data.name").value("new-cert"))
+                .andExpect(jsonPath("$.data.namespace").value("default"))
+                .andExpect(jsonPath("$.data.cluster").value("test-cluster"))
+                .andExpect(jsonPath("$.data.type").value("TLS"))
+                .andExpect(jsonPath("$.data.status").value("valid"))
+                .andExpect(jsonPath("$.data.san[0]").value("svc.example.com"));
+    }
+
+    @Test
+    void createCertShouldAcceptMinimalCommand() throws Exception {
+        K8sCertVO createdCert = buildCert("cert-min", "minimal-cert", CertType.TLS, CertStatus.valid);
+        when(k8sCertService.createCert(any(CreateCertDTO.class))).thenReturn(createdCert);
+
+        String json = """
+                {
+                    "name": "minimal-cert",
+                    "namespace": "default",
+                    "cluster": "test",
+                    "type": "TLS",
+                    "issuer": "test-issuer"
+                }
+                """;
+
+        mockMvc.perform(post("/api/k8s-certs/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(json))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("cert-min"));
+    }
+
+    private K8sCertVO buildCert(String id, String name, CertType type, CertStatus status) {
+        K8sCertVO cert = K8sCertVO.builder()
+                .name(name)
+                .namespace("mq-system")
+                .cluster("prod-cluster")
+                .type(type)
+                .issuer("letsencrypt")
+                .notBefore(LocalDateTime.of(2025, 1, 1, 0, 0))
+                .notAfter(LocalDateTime.of(2026, 1, 1, 0, 0))
+                .status(status)
+                .daysRemaining(180)
+                .san(List.of("example.com"))
+                .build();
+        cert.setId(id);
+        return cert;
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
new file mode 100644
index 0000000..d886c0e
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
@@ -0,0 +1,268 @@
+/*
+ * 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 com.rocketmq.studio.cluster.k8s;
+
+import com.rocketmq.studio.common.domain.enums.CertStatus;
+import com.rocketmq.studio.common.domain.enums.CertType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class K8sCertServiceTest {
+
+    @Mock
+    private K8sCertRepository k8sCertRepository;
+
+    @InjectMocks
+    private K8sCertService k8sCertService;
+
+    private K8sCertVO sampleCert;
+
+    @BeforeEach
+    void setUp() {
+        sampleCert = K8sCertVO.builder()
+                .name("rocketmq-tls")
+                .namespace("mq-system")
+                .cluster("prod-cluster")
+                .type(CertType.TLS)
+                .issuer("letsencrypt")
+                .notBefore(LocalDateTime.of(2025, 1, 1, 0, 0))
+                .notAfter(LocalDateTime.of(2026, 1, 1, 0, 0))
+                .status(CertStatus.valid)
+                .daysRemaining(180)
+                .san(Arrays.asList("rocketmq.example.com", "*.rocketmq.example.com"))
+                .build();
+        sampleCert.setId("cert-1");
+    }
+
+    @Test
+    void listCertsShouldReturnAllCerts() {
+        K8sCertVO secondCert = K8sCertVO.builder()
+                .name("broker-mtls")
+                .type(CertType.mTLS)
+                .status(CertStatus.expiring)
+                .build();
+        secondCert.setId("cert-2");
+
+        when(k8sCertRepository.findAll()).thenReturn(Arrays.asList(sampleCert, secondCert));
+
+        List<K8sCertVO> result = k8sCertService.listCerts();
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getName()).isEqualTo("rocketmq-tls");
+        assertThat(result.get(0).getType()).isEqualTo(CertType.TLS);
+        assertThat(result.get(1).getName()).isEqualTo("broker-mtls");
+        assertThat(result.get(1).getType()).isEqualTo(CertType.mTLS);
+        verify(k8sCertRepository).findAll();
+    }
+
+    @Test
+    void listCertsShouldReturnEmptyListWhenNoCerts() {
+        when(k8sCertRepository.findAll()).thenReturn(Collections.emptyList());
+
+        List<K8sCertVO> result = k8sCertService.listCerts();
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void createCertShouldCreateAndSaveCert() {
+        CreateCertDTO command = CreateCertDTO.builder()
+                .name("new-tls-cert")
+                .namespace("default")
+                .cluster("test-cluster")
+                .type("TLS")
+                .issuer("vault")
+                .san(List.of("svc.example.com"))
+                .build();
+
+        when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> {
+            K8sCertVO cert = invocation.getArgument(0);
+            if (cert.getId() == null) {
+                cert.setId("generated-id");
+            }
+            return cert;
+        });
+
+        K8sCertVO result = k8sCertService.createCert(command);
+
+        assertThat(result.getName()).isEqualTo("new-tls-cert");
+        assertThat(result.getNamespace()).isEqualTo("default");
+        assertThat(result.getCluster()).isEqualTo("test-cluster");
+        assertThat(result.getType()).isEqualTo(CertType.TLS);
+        assertThat(result.getIssuer()).isEqualTo("vault");
+        assertThat(result.getStatus()).isEqualTo(CertStatus.valid);
+        assertThat(result.getSan()).containsExactly("svc.example.com");
+        assertThat(result.getNotBefore()).isNotNull();
+        assertThat(result.getNotAfter()).isAfter(result.getNotBefore());
+        assertThat(result.getDaysRemaining()).isGreaterThan(0);
+        verify(k8sCertRepository).save(any(K8sCertVO.class));
+    }
+
+    @Test
+    void createCertShouldSetCorrectValidityPeriod() {
+        CreateCertDTO command = CreateCertDTO.builder()
+                .name("validity-test")
+                .namespace("default")
+                .cluster("test-cluster")
+                .type("TLS")
+                .issuer("test-issuer")
+                .build();
+
+        ArgumentCaptor<K8sCertVO> captor = ArgumentCaptor.forClass(K8sCertVO.class);
+        when(k8sCertRepository.save(captor.capture())).thenAnswer(invocation -> invocation.getArgument(0));
+
+        k8sCertService.createCert(command);
+
+        K8sCertVO saved = captor.getValue();
+        assertThat(saved.getNotAfter()).isAfter(saved.getNotBefore());
+        long expectedDays = java.time.temporal.ChronoUnit.DAYS.between(saved.getNotBefore(), saved.getNotAfter());
+        assertThat(saved.getDaysRemaining()).isEqualTo((int) expectedDays);
+    }
+
+    @Test
+    void updateCertShouldUpdateFieldsWhenFound() {
+        when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+        when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        UpdateCertDTO command = UpdateCertDTO.builder()
+                .id("cert-1")
+                .name("updated-name")
+                .namespace("new-namespace")
+                .cluster("new-cluster")
+                .type("mTLS")
+                .issuer("new-issuer")
+                .san(List.of("new.example.com"))
+                .build();
+
+        K8sCertVO result = k8sCertService.updateCert(command);
+
+        assertThat(result.getName()).isEqualTo("updated-name");
+        assertThat(result.getNamespace()).isEqualTo("new-namespace");
+        assertThat(result.getCluster()).isEqualTo("new-cluster");
+        assertThat(result.getType()).isEqualTo(CertType.mTLS);
+        assertThat(result.getIssuer()).isEqualTo("new-issuer");
+        assertThat(result.getSan()).containsExactly("new.example.com");
+        assertThat(result.getUpdatedAt()).isNotNull();
+        verify(k8sCertRepository).save(any(K8sCertVO.class));
+    }
+
+    @Test
+    void updateCertShouldPreserveExistingFieldsWhenCommandFieldsAreNull() {
+        when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+        when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        UpdateCertDTO command = UpdateCertDTO.builder()
+                .id("cert-1")
+                .name("only-name-changed")
+                .build();
+
+        K8sCertVO result = k8sCertService.updateCert(command);
+
+        assertThat(result.getName()).isEqualTo("only-name-changed");
+        assertThat(result.getNamespace()).isEqualTo("mq-system");
+        assertThat(result.getCluster()).isEqualTo("prod-cluster");
+        assertThat(result.getType()).isEqualTo(CertType.TLS);
+        assertThat(result.getIssuer()).isEqualTo("letsencrypt");
+    }
+
+    @Test
+    void updateCertShouldThrowWhenNotFound() {
+        when(k8sCertRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        UpdateCertDTO command = UpdateCertDTO.builder()
+                .id("nonexistent")
+                .name("wont-work")
+                .build();
+
+        assertThatThrownBy(() -> k8sCertService.updateCert(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Certificate not found: nonexistent")
+                .satisfies(ex -> assertThat(((BusinessException) ex).getCode()).isEqualTo(404));
+    }
+
+    @Test
+    void renewCertShouldRenewCertValidity() {
+        sampleCert.setStatus(CertStatus.expired);
+        sampleCert.setDaysRemaining(0);
+
+        when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+        when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        RenewCertDTO command = RenewCertDTO.builder().id("cert-1").build();
+
+        K8sCertVO result = k8sCertService.renewCert(command);
+
+        assertThat(result.getStatus()).isEqualTo(CertStatus.valid);
+        assertThat(result.getDaysRemaining()).isGreaterThan(0);
+        assertThat(result.getNotBefore()).isNotNull();
+        assertThat(result.getNotAfter()).isAfter(result.getNotBefore());
+        assertThat(result.getUpdatedAt()).isNotNull();
+        verify(k8sCertRepository).save(any(K8sCertVO.class));
+    }
+
+    @Test
+    void renewCertShouldThrowWhenNotFound() {
+        when(k8sCertRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        RenewCertDTO command = RenewCertDTO.builder().id("nonexistent").build();
+
+        assertThatThrownBy(() -> k8sCertService.renewCert(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Certificate not found: nonexistent");
+    }
+
+    @Test
+    void deleteCertShouldDeleteWhenFound() {
+        when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+
+        DeleteCertDTO command = DeleteCertDTO.builder().id("cert-1").build();
+
+        k8sCertService.deleteCert(command);
+
+        verify(k8sCertRepository).deleteById("cert-1");
+    }
+
+    @Test
+    void deleteCertShouldThrowWhenNotFound() {
+        when(k8sCertRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        DeleteCertDTO command = DeleteCertDTO.builder().id("nonexistent").build();
+
+        assertThatThrownBy(() -> k8sCertService.deleteCert(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Certificate not found: nonexistent");
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/cluster/metrics/MetricsServiceTest.java b/server/src/test/java/com/rocketmq/studio/cluster/metrics/MetricsServiceTest.java
new file mode 100644
index 0000000..b689775
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/cluster/metrics/MetricsServiceTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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 com.rocketmq.studio.cluster.metrics;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MetricsServiceTest {
+
+    @Mock
+    private MetricsSource metricsSource;
+
+    @InjectMocks
+    private MetricsService metricsService;
+
+    @Test
+    void queryShouldReturnMetricData() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("cpu_usage")
+                .start(1700000000L)
+                .end(1700003600L)
+                .step("1m")
+                .build();
+        List<long[]> values = Arrays.asList(
+                new long[]{1700000000L, 45},
+                new long[]{1700000060L, 52},
+                new long[]{1700000120L, 48}
+        );
+        MetricDataVO data = MetricDataVO.builder().metric("cpu_usage").values(values).build();
+        when(metricsSource.query(query)).thenReturn(data);
+
+        MetricDataVO result = metricsService.query(query);
+
+        assertThat(result.getMetric()).isEqualTo("cpu_usage");
+        assertThat(result.getValues()).hasSize(3);
+        assertThat(result.getValues().get(0)[0]).isEqualTo(1700000000L);
+        assertThat(result.getValues().get(0)[1]).isEqualTo(45);
+        assertThat(result.getValues().get(1)[1]).isEqualTo(52);
+        assertThat(result.getValues().get(2)[1]).isEqualTo(48);
+        verify(metricsSource).query(query);
+    }
+
+    @Test
+    void queryShouldReturnEmptyValuesWhenNoData() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("disk_io")
+                .start(1700000000L)
+                .end(1700003600L)
+                .step("5m")
+                .build();
+        MetricDataVO data = MetricDataVO.builder().metric("disk_io").values(Collections.emptyList()).build();
+        when(metricsSource.query(query)).thenReturn(data);
+
+        MetricDataVO result = metricsService.query(query);
+
+        assertThat(result.getMetric()).isEqualTo("disk_io");
+        assertThat(result.getValues()).isEmpty();
+    }
+
+    @Test
+    void queryShouldPassQueryDirectlyToSource() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("tps")
+                .start(1700000000L)
+                .end(1700086400L)
+                .step("1h")
+                .build();
+        MetricDataVO data = MetricDataVO.builder().metric("tps").values(Collections.emptyList()).build();
+        when(metricsSource.query(any(MetricQueryDTO.class))).thenReturn(data);
+
+        metricsService.query(query);
+
+        verify(metricsSource).query(query);
+    }
+
+    @Test
+    void queryShouldHandleVariousStepSizes() {
+        MetricQueryDTO query15s = MetricQueryDTO.builder().metric("cpu").start(1L).end(2L).step("15s").build();
+        MetricQueryDTO query1h = MetricQueryDTO.builder().metric("cpu").start(1L).end(2L).step("1h").build();
+        MetricDataVO data = MetricDataVO.builder().metric("cpu").values(Collections.emptyList()).build();
+        when(metricsSource.query(any(MetricQueryDTO.class))).thenReturn(data);
+
+        MetricDataVO result15s = metricsService.query(query15s);
+        MetricDataVO result1h = metricsService.query(query1h);
+
+        assertThat(result15s).isNotNull();
+        assertThat(result1h).isNotNull();
+        verify(metricsSource).query(query15s);
+        verify(metricsSource).query(query1h);
+    }
+
+    @Test
+    void queryShouldReturnMultipleDataPoints() {
+        MetricQueryDTO query = MetricQueryDTO.builder()
+                .metric("memory_usage")
+                .start(1700000000L)
+                .end(1700003600L)
+                .step("1m")
+                .build();
+        List<long[]> values = Arrays.asList(
+                new long[]{1700000000L, 72},
+                new long[]{1700000060L, 73},
+                new long[]{1700000120L, 71},
+                new long[]{1700000180L, 74},
+                new long[]{1700000240L, 75}
+        );
+        MetricDataVO data = MetricDataVO.builder().metric("memory_usage").values(values).build();
+        when(metricsSource.query(query)).thenReturn(data);
+
+        MetricDataVO result = metricsService.query(query);
+
+        assertThat(result.getMetric()).isEqualTo("memory_usage");
+        assertThat(result.getValues()).hasSize(5);
+    }
+
+    @Test
+    void queryShouldPreserveMetricName() {
+        String[] metrics = {"rocketmq_tps", "rocketmq_latency_p99", "broker_disk_usage", "consumer_lag"};
+        for (String metricName : metrics) {
+            MetricQueryDTO query = MetricQueryDTO.builder().metric(metricName).start(1L).end(2L).step("1m").build();
+            MetricDataVO data = MetricDataVO.builder().metric(metricName).values(Collections.emptyList()).build();
+            when(metricsSource.query(query)).thenReturn(data);
+
+            MetricDataVO result = metricsService.query(query);
+
+            assertThat(result.getMetric()).isEqualTo(metricName);
+        }
+    }
+}


(rocketmq-dashboard) 04/09: feat: scaffold Spring Boot backend with Maven

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit 8ca0dae6375f2cf8e6981d318afee857fe9f4f07
Author: lizhimins <70...@qq.com>
AuthorDate: Sat Jul 4 10:18:00 2026 +0800

    feat: scaffold Spring Boot backend with Maven
    
    - Initialize Spring Boot 3.5.3 + Java 21 project
    - Configure Maven with checkstyle plugin (RocketMQ style)
    - Add application.yml with dev/prod profiles
    - Configure logging with logback-spring.xml
    - Add custom banner.txt
    - Add Dockerfile for backend deployment
---
 server/.gitignore                                  |  10 +
 server/Dockerfile                                  |  16 ++
 server/LICENSE                                     | 201 +++++++++++++++++++++
 server/NOTICE                                      |   5 +
 server/pom.xml                                     |  89 +++++++++
 .../com/rocketmq/studio/StudioApplication.java     |  28 +++
 server/src/main/resources/application-dev.yml      |   5 +
 server/src/main/resources/application-prod.yml     |   5 +
 server/src/main/resources/application.yml          |  14 ++
 server/src/main/resources/banner.txt               |   7 +
 server/src/main/resources/logback-spring.xml       |  13 ++
 server/style/rmq_checkstyle.xml                    | 140 ++++++++++++++
 12 files changed, 533 insertions(+)

diff --git a/server/.gitignore b/server/.gitignore
new file mode 100644
index 0000000..b483754
--- /dev/null
+++ b/server/.gitignore
@@ -0,0 +1,10 @@
+target/
+*.class
+*.jar
+*.war
+*.log
+.idea/
+*.iml
+.settings/
+.project
+.classpath
diff --git a/server/Dockerfile b/server/Dockerfile
new file mode 100644
index 0000000..057cb91
--- /dev/null
+++ b/server/Dockerfile
@@ -0,0 +1,16 @@
+# Stage 1: Build
+FROM alibabadragonwell/dragonwell:21 AS build
+RUN curl -fsSL https://archive.apache.org/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz | tar xz -C /opt \
+    && ln -s /opt/apache-maven-3.9.9/bin/mvn /usr/local/bin/mvn
+WORKDIR /app
+COPY pom.xml .
+RUN mvn dependency:go-offline
+COPY src ./src
+RUN mvn package -DskipTests
+
+# Stage 2: Runtime
+FROM alibabadragonwell/dragonwell:21
+WORKDIR /app
+COPY --from=build /app/target/*.jar app.jar
+EXPOSE 8888
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/server/LICENSE b/server/LICENSE
new file mode 100644
index 0000000..c8aa782
--- /dev/null
+++ b/server/LICENSE
@@ -0,0 +1,201 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to the Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by the Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding any notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. Please also get an in-depth
+      understanding of the Apache License by reading the FAQ at
+      http://www.apache.org/foundation/license-faq.html
+
+   Copyright 2026 RocketMQ Studio Contributors
+
+   Licensed 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.
diff --git a/server/NOTICE b/server/NOTICE
new file mode 100644
index 0000000..fde8f8e
--- /dev/null
+++ b/server/NOTICE
@@ -0,0 +1,5 @@
+RocketMQ Studio
+Copyright 2026 RocketMQ Studio Contributors
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/server/pom.xml b/server/pom.xml
new file mode 100644
index 0000000..55e3e94
--- /dev/null
+++ b/server/pom.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.5.3</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>com.rocketmq</groupId>
+    <artifactId>rocketmq-studio</artifactId>
+    <version>1.0.0</version>
+    <name>RocketMQ Studio</name>
+
+    <properties>
+        <java.version>21</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+            <version>2.8.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.tngtech.archunit</groupId>
+            <artifactId>archunit-junit5</artifactId>
+            <version>1.3.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <version>3.6.0</version>
+                <configuration>
+                    <configLocation>style/rmq_checkstyle.xml</configLocation>
+                    <consoleOutput>true</consoleOutput>
+                    <failsOnError>true</failsOnError>
+                    <includeTestSourceDirectory>true</includeTestSourceDirectory>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>validate</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/server/src/main/java/com/rocketmq/studio/StudioApplication.java b/server/src/main/java/com/rocketmq/studio/StudioApplication.java
new file mode 100644
index 0000000..39feaeb
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/StudioApplication.java
@@ -0,0 +1,28 @@
+/*
+ * 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 com.rocketmq.studio;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class StudioApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(StudioApplication.class, args);
+    }
+}
diff --git a/server/src/main/resources/application-dev.yml b/server/src/main/resources/application-dev.yml
new file mode 100644
index 0000000..9dbdaf1
--- /dev/null
+++ b/server/src/main/resources/application-dev.yml
@@ -0,0 +1,5 @@
+# Development profile
+logging:
+  level:
+    com.rocketmq.studio: DEBUG
+    org.springframework.web: DEBUG
diff --git a/server/src/main/resources/application-prod.yml b/server/src/main/resources/application-prod.yml
new file mode 100644
index 0000000..8df9da5
--- /dev/null
+++ b/server/src/main/resources/application-prod.yml
@@ -0,0 +1,5 @@
+# Production profile
+logging:
+  level:
+    com.rocketmq.studio: INFO
+    org.springframework.web: WARN
diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml
new file mode 100644
index 0000000..3c98b4c
--- /dev/null
+++ b/server/src/main/resources/application.yml
@@ -0,0 +1,14 @@
+server:
+  port: 8888
+
+spring:
+  application:
+    name: rocketmq-studio
+  profiles:
+    active: dev
+
+springdoc:
+  api-docs:
+    path: /api-docs
+  swagger-ui:
+    path: /swagger-ui.html
diff --git a/server/src/main/resources/banner.txt b/server/src/main/resources/banner.txt
new file mode 100644
index 0000000..e6bf11b
--- /dev/null
+++ b/server/src/main/resources/banner.txt
@@ -0,0 +1,7 @@
+ _______                   __             _   ____    ____   ___    ___ ___
+|_   __ \                 [  |  _        / |_|_   \  /   _|.'   `.   \ \ \ \
+  | |__) |   .--.   .---.  | | / ] .---.`| |-' |   \/   | /  .-.  \    \ \ \ \
+  |  __ /  / .'`\ \/ /'`\] | '' < / /__\\| |   | |\  /| | | |   | |     ) ) ) )
+ _| |  \ \_| \__. || \__.  | |`\ \| \__.,| |, _| |_\/_| |_\  `-'  \_   / / / /
+|____| |___|'.__.' '.___.'[__|  \_]'.__.'\__/|_____||_____|`.___.\__| /_/_/_/
+ :: RocketMQ Studio ::
diff --git a/server/src/main/resources/logback-spring.xml b/server/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..274690a
--- /dev/null
+++ b/server/src/main/resources/logback-spring.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
+
+    <property name="CONSOLE_LOG_PATTERN"
+              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n"/>
+
+    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
+
+    <root level="INFO">
+        <appender-ref ref="CONSOLE"/>
+    </root>
+</configuration>
diff --git a/server/style/rmq_checkstyle.xml b/server/style/rmq_checkstyle.xml
new file mode 100644
index 0000000..0fb549b
--- /dev/null
+++ b/server/style/rmq_checkstyle.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+
+<!DOCTYPE module PUBLIC
+    "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
+    "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+<!--Refer http://checkstyle.sourceforge.net/reports/google-java-style.html#s2.2-file-encoding -->
+<module name="Checker">
+
+    <property name="localeLanguage" value="en"/>
+
+    <!--To configure the check to report on the first instance in each file-->
+    <module name="FileTabCharacter"/>
+
+    <!-- header -->
+    <module name="RegexpHeader">
+        <property name="header" value="/\*\nLicensed to the Apache Software Foundation*"/>
+        <property name="fileExtensions" value="java"/>
+    </module>
+
+    <module name="RegexpHeader">
+        <property name="header" value="#[\s]*Licensed to the Apache Software Foundation*"/>
+        <property name="fileExtensions" value="properties"/>
+    </module>
+
+    <module name="RegexpSingleline">
+        <property name="format" value="System\.out\.println"/>
+        <property name="message" value="Prohibit invoking System.out.println in source code !"/>
+    </module>
+
+    <module name="RegexpSingleline">
+        <property name="format" value="//FIXME"/>
+        <property name="message" value="Recommended fix FIXME task !"/>
+    </module>
+
+    <module name="RegexpSingleline">
+        <property name="format" value="//TODO"/>
+        <property name="message" value="Recommended fix TODO task !"/>
+    </module>
+
+    <module name="RegexpSingleline">
+        <property name="format" value="@alibaba"/>
+        <property name="message" value="Recommended remove @alibaba keyword!"/>
+    </module>
+    <module name="RegexpSingleline">
+        <property name="format" value="@taobao"/>
+        <property name="message" value="Recommended remove @taobao keyword!"/>
+    </module>
+    <module name="RegexpSingleline">
+        <property name="format" value="@author"/>
+        <property name="message" value="Recommended remove @author tag in javadoc!"/>
+    </module>
+
+    <module name="RegexpSingleline">
+        <property name="format"
+                  value=".*[\u3400-\u4DB5\u4E00-\u9FA5\u9FA6-\u9FBB\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9\uFF00-\uFFEF\u2E80-\u2EFF\u3000-\u303F\u31C0-\u31EF]+.*"/>
+        <property name="message" value="Not allow chinese character !"/>
+    </module>
+
+    <module name="FileLength">
+        <property name="max" value="5000"/>
+    </module>
+
+    <module name="TreeWalker">
+
+        <module name="UnusedImports">
+            <property name="processJavadoc" value="true"/>
+        </module>
+        <module name="RedundantImport"/>
+
+        <!--<module name="IllegalImport" />-->
+
+        <!--Checks that classes that override equals() also override hashCode()-->
+        <module name="EqualsHashCode"/>
+        <!--Checks for over-complicated boolean expressions. Currently finds code like if (topic == true), topic || true, !false, etc.-->
+        <module name="SimplifyBooleanExpression"/>
+        <module name="OneStatementPerLine"/>
+        <module name="UnnecessaryParentheses"/>
+        <!--Checks for over-complicated boolean return statements. For example the following code-->
+        <module name="SimplifyBooleanReturn"/>
+
+        <!--Check that the default is after all the cases in producerGroup switch statement-->
+        <module name="DefaultComesLast"/>
+        <!--Detects empty statements (standalone ";" semicolon)-->
+        <module name="EmptyStatement"/>
+        <!--Checks that long constants are defined with an upper ell-->
+        <module name="UpperEll"/>
+        <module name="ConstantName">
+            <property name="format" value="(^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$)|(^log[a-zA-Z0-9]*$)"/>
+        </module>
+        <!--Checks that local, non-final variable names conform to producerGroup format specified by the format property-->
+        <module name="LocalVariableName"/>
+        <!--Validates identifiers for local, final variables, including catch parameters-->
+        <module name="LocalFinalVariableName"/>
+        <!--Validates identifiers for non-static fields-->
+        <module name="MemberName"/>
+        <!--Validates identifiers for class type parameters-->
+        <module name="ClassTypeParameterName">
+            <property name="format" value="^[A-Z0-9]*$"/>
+        </module>
+        <!--Validates identifiers for method type parameters-->
+        <module name="MethodTypeParameterName">
+            <property name="format" value="^[A-Z0-9]*$"/>
+        </module>
+        <module name="PackageName"/>
+        <module name="ParameterName"/>
+        <module name="StaticVariableName"/>
+        <module name="TypeName"/>
+        <!--Checks that there are no import statements that use the * notation-->
+        <module name="AvoidStarImport"/>
+
+        <!--whitespace-->
+        <module name="GenericWhitespace"/>
+        <!--<module name="NoWhitespaceBefore"/>-->
+        <!--<module name="NoWhitespaceAfter"/>-->
+        <module name="WhitespaceAround">
+            <property name="allowEmptyConstructors" value="true"/>
+            <property name="allowEmptyMethods" value="true"/>
+        </module>
+        <module name="Indentation"/>
+        <module name="MethodParamPad"/>
+        <module name="ParenPad"/>
+        <module name="TypecastParenPad"/>
+    </module>
+</module>


(rocketmq-dashboard) 02/09: feat: scaffold frontend with React + Vite + Tailwind

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit 730ec6cb16f60d82758bb54491e58ea35215980a
Author: lizhimins <70...@qq.com>
AuthorDate: Thu Jul 2 14:23:00 2026 +0800

    feat: scaffold frontend with React + Vite + Tailwind
    
    - Initialize React 18 + TypeScript + Vite project
    - Configure Tailwind CSS and PostCSS
    - Add Ant Design component library
    - Set up ESLint and Prettier
    - Create main layout with sidebar navigation
    - Add Docker build config for frontend
---
 web/.env                       |    2 +
 web/.env.production            |    3 +
 web/.gitignore                 |    5 +
 web/.prettierignore            |    3 +
 web/.prettierrc                |    9 +
 web/Dockerfile                 |   13 +
 web/LICENSE                    |  201 ++
 web/NOTICE                     |    5 +
 web/eslint.config.js           |   26 +
 web/index.html                 |   12 +
 web/nginx.conf                 |   20 +
 web/package-lock.json          | 5841 ++++++++++++++++++++++++++++++++++++++++
 web/package.json               |   55 +
 web/postcss.config.cjs         |    6 +
 web/src/App.tsx                |   63 +
 web/src/config.ts              |    7 +
 web/src/constants/theme.ts     |   57 +
 web/src/hooks/useApi.ts        |   46 +
 web/src/index.css              |  184 ++
 web/src/layouts/MainLayout.tsx |  400 +++
 web/src/main.tsx               |   62 +
 web/src/vite-env.d.ts          |   18 +
 web/tailwind.config.cjs        |    8 +
 web/tsconfig.app.json          |   32 +
 web/tsconfig.json              |    7 +
 web/tsconfig.node.json         |   24 +
 web/vite.config.ts             |   15 +
 27 files changed, 7124 insertions(+)

diff --git a/web/.env b/web/.env
new file mode 100644
index 0000000..d3264de
--- /dev/null
+++ b/web/.env
@@ -0,0 +1,2 @@
+# Set to "true" to use mock data, "false" to use real API
+VITE_USE_MOCK=true
diff --git a/web/.env.production b/web/.env.production
new file mode 100644
index 0000000..3e82798
--- /dev/null
+++ b/web/.env.production
@@ -0,0 +1,3 @@
+# Backend not ready yet - use mock data
+# Set to "false" when real API is available
+VITE_USE_MOCK=true
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..7e9e25d
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+*.local
+.env.local
+.env.*.local
diff --git a/web/.prettierignore b/web/.prettierignore
new file mode 100644
index 0000000..e47db50
--- /dev/null
+++ b/web/.prettierignore
@@ -0,0 +1,3 @@
+dist
+node_modules
+*.html
diff --git a/web/.prettierrc b/web/.prettierrc
new file mode 100644
index 0000000..f800d4e
--- /dev/null
+++ b/web/.prettierrc
@@ -0,0 +1,9 @@
+{
+  "semi": true,
+  "singleQuote": true,
+  "trailingComma": "all",
+  "printWidth": 100,
+  "tabWidth": 2,
+  "arrowParens": "always",
+  "endOfLine": "lf"
+}
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 0000000..0998464
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,13 @@
+# Stage 1: Build
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+COPY . .
+RUN npm run build
+
+# Stage 2: Serve
+FROM nginx:alpine
+COPY --from=build /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+EXPOSE 80
diff --git a/web/LICENSE b/web/LICENSE
new file mode 100644
index 0000000..c8aa782
--- /dev/null
+++ b/web/LICENSE
@@ -0,0 +1,201 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to the Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by the Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding any notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. Please also get an in-depth
+      understanding of the Apache License by reading the FAQ at
+      http://www.apache.org/foundation/license-faq.html
+
+   Copyright 2026 RocketMQ Studio Contributors
+
+   Licensed 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.
diff --git a/web/NOTICE b/web/NOTICE
new file mode 100644
index 0000000..fde8f8e
--- /dev/null
+++ b/web/NOTICE
@@ -0,0 +1,5 @@
+RocketMQ Studio
+Copyright 2026 RocketMQ Studio Contributors
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/web/eslint.config.js b/web/eslint.config.js
new file mode 100644
index 0000000..cb4f6fc
--- /dev/null
+++ b/web/eslint.config.js
@@ -0,0 +1,26 @@
+import js from '@eslint/js';
+import tseslint from 'typescript-eslint';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import prettier from 'eslint-config-prettier';
+
+export default tseslint.config(
+  { ignores: ['dist'] },
+  {
+    extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
+    files: ['**/*.{ts,tsx}'],
+    languageOptions: {
+      ecmaVersion: 2020,
+    },
+    plugins: {
+      'react-hooks': reactHooks,
+      'react-refresh': reactRefresh,
+    },
+    rules: {
+      ...reactHooks.configs.recommended.rules,
+      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+      '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
+      '@typescript-eslint/no-explicit-any': 'warn',
+    },
+  },
+);
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..11fb8ba
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>RocketMQ Studio</title>
+</head>
+<body>
+  <div id="root"></div>
+  <script type="module" src="/src/main.tsx"></script>
+</body>
+</html>
diff --git a/web/nginx.conf b/web/nginx.conf
new file mode 100644
index 0000000..165c0f9
--- /dev/null
+++ b/web/nginx.conf
@@ -0,0 +1,20 @@
+server {
+    listen 80;
+    server_name _;
+
+    location / {
+        root /usr/share/nginx/html;
+        index index.html;
+        try_files $uri $uri/ /index.html;
+    }
+
+    location /api/ {
+        resolver 10.89.0.1 valid=10s;
+        set $backend http://rocketmq-server:8888;
+        proxy_pass $backend;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..647e869
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,5841 @@
+{
+  "name": "rocketmq-studio-web",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "rocketmq-studio-web",
+      "version": "0.1.0",
+      "dependencies": {
+        "@ant-design/icons": "^5.5.0",
+        "@phosphor-icons/react": "^2.1.10",
+        "antd": "^5.22.0",
+        "axios": "^1.7.0",
+        "react": "^18.3.1",
+        "react-dom": "^18.3.1",
+        "react-router-dom": "^7.0.0",
+        "zustand": "^5.0.0"
+      },
+      "devDependencies": {
+        "@eslint/js": "^10.0.1",
+        "@types/react": "^18.3.0",
+        "@types/react-dom": "^18.3.0",
+        "@typescript-eslint/eslint-plugin": "^8.62.1",
+        "@typescript-eslint/parser": "^8.62.1",
+        "@vitejs/plugin-react": "^4.3.0",
+        "autoprefixer": "^10.5.2",
+        "eslint": "^10.6.0",
+        "eslint-config-prettier": "^10.1.8",
+        "eslint-plugin-react-hooks": "^7.1.1",
+        "eslint-plugin-react-refresh": "^0.5.3",
+        "husky": "^9.1.7",
+        "lint-staged": "^16.4.0",
+        "postcss": "^8.5.16",
+        "prettier": "^3.9.4",
+        "tailwindcss": "^3.4.19",
+        "typescript": "^5.6.0",
+        "typescript-eslint": "^8.62.1",
+        "vite": "^6.0.0"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@ant-design/colors": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
+      "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/fast-color": "^2.0.6"
+      }
+    },
+    "node_modules/@ant-design/cssinjs": {
+      "version": "1.24.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz",
+      "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "@emotion/hash": "^0.8.0",
+        "@emotion/unitless": "^0.7.5",
+        "classnames": "^2.3.1",
+        "csstype": "^3.1.3",
+        "rc-util": "^5.35.0",
+        "stylis": "^4.3.4"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/@ant-design/cssinjs-utils": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz",
+      "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/cssinjs": "^1.21.0",
+        "@babel/runtime": "^7.23.2",
+        "rc-util": "^5.38.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@ant-design/fast-color": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
+      "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7"
+      },
+      "engines": {
+        "node": ">=8.x"
+      }
+    },
+    "node_modules/@ant-design/icons": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
+      "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^7.0.0",
+        "@ant-design/icons-svg": "^4.4.0",
+        "@babel/runtime": "^7.24.8",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.31.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz",
+      "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/react-slick": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
+      "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.4",
+        "classnames": "^2.2.5",
+        "json2mq": "^0.2.0",
+        "resize-observer-polyfill": "^1.5.1",
+        "throttle-debounce": "^5.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+      "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+      "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-compilation-targets": "^7.29.7",
+        "@babel/helper-module-transforms": "^7.29.7",
+        "@babel/helpers": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+      "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+      "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.29.7",
+        "@babel/helper-validator-option": "^7.29.7",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+      "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+      "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+      "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "@babel/traverse": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+      "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+      "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+      "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.7"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+      "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+      "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+      "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+      "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+      "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-globals": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
+      "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+      "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
+      "license": "MIT"
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.9.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+      "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+      "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.23.5",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+      "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/object-schema": "^3.0.5",
+        "debug": "^4.3.1",
+        "minimatch": "^10.2.4"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+      "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^1.2.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+      "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+      "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "eslint": "^10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@eslint/object-schema": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+      "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+      "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^1.2.1",
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+      "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/types": "^0.15.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.8",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+      "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.2",
+        "@humanfs/types": "^0.15.0",
+        "@humanwhocodes/retry": "^0.4.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/types": {
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+      "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/retry": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@phosphor-icons/react": {
+      "version": "2.1.10",
+      "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
+      "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8",
+        "react-dom": ">= 16.8"
+      }
+    },
+    "node_modules/@rc-component/async-validator": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.2.tgz",
+      "integrity": "sha512-WYbrZSjzznU1ekD0qFq2qRxt309VoS61MTG5npnFQlKYcoy9IzU8T+ZCIhq5bGAXRbXysABFWTspicMfmWFwow==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.4"
+      },
+      "engines": {
+        "node": ">=14.x"
+      }
+    },
+    "node_modules/@rc-component/color-picker": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
+      "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/fast-color": "^2.0.6",
+        "@babel/runtime": "^7.23.6",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.38.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/context": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz",
+      "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/mini-decimal": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz",
+      "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      }
+    },
+    "node_modules/@rc-component/mutate-observer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
+      "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/portal": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz",
+      "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/qrcode": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.3.tgz",
+      "integrity": "sha512-aGv6alnn4HbDEsURzKP+jv13rbi1VxmAYfBNZr5GKF1iohMNWy5tAVoJ1E3cOvzMB1kbUPvCXchM6zSFlRGPhA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/tour": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz",
+      "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "@rc-component/portal": "^1.0.0-9",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/trigger": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz",
+      "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "@rc-component/portal": "^1.1.0",
+        "classnames": "^2.3.2",
+        "rc-motion": "^2.0.0",
+        "rc-resize-observer": "^1.3.1",
+        "rc-util": "^5.44.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+      "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+      "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+      "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+      "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+      "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+      "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+      "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+      "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+      "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+      "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+      "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+      "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+      "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+      "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+      "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+      "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+      "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+      "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+      "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+      "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+      "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+      "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+      "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+      "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+      "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/esrecurse": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+      "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/prop-types": {
+      "version": "15.7.15",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/react": {
+      "version": "18.3.31",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+      "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/prop-types": "*",
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "18.3.7",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+      "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^18.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
+      "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.12.2",
+        "@typescript-eslint/scope-manager": "8.62.1",
+        "@typescript-eslint/type-utils": "8.62.1",
+        "@typescript-eslint/utils": "8.62.1",
+        "@typescript-eslint/visitor-keys": "8.62.1",
+        "ignore": "^7.0.5",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^8.62.1",
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
+      "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.62.1",
+        "@typescript-eslint/types": "8.62.1",
+        "@typescript-eslint/typescript-estree": "8.62.1",
+        "@typescript-eslint/visitor-keys": "8.62.1",
+        "debug": "^4.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/project-service": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
+      "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/tsconfig-utils": "^8.62.1",
+        "@typescript-eslint/types": "^8.62.1",
+        "debug": "^4.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
+      "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.62.1",
+        "@typescript-eslint/visitor-keys": "8.62.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/tsconfig-utils": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
+      "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
+      "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.62.1",
+        "@typescript-eslint/typescript-estree": "8.62.1",
+        "@typescript-eslint/utils": "8.62.1",
+        "debug": "^4.4.3",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
+      "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
+      "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/project-service": "8.62.1",
+        "@typescript-eslint/tsconfig-utils": "8.62.1",
+        "@typescript-eslint/types": "8.62.1",
+        "@typescript-eslint/visitor-keys": "8.62.1",
+        "debug": "^4.4.3",
+        "minimatch": "^10.2.2",
+        "semver": "^7.7.3",
+        "tinyglobby": "^0.2.15",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
+      "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.9.1",
+        "@typescript-eslint/scope-manager": "8.62.1",
+        "@typescript-eslint/types": "8.62.1",
+        "@typescript-eslint/typescript-estree": "8.62.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
+      "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.62.1",
+        "eslint-visitor-keys": "^5.0.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+      "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.27",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.17.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+      "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.15.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+      "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
+      "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "environment": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/antd": {
+      "version": "5.29.3",
+      "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz",
+      "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^7.2.1",
+        "@ant-design/cssinjs": "^1.23.0",
+        "@ant-design/cssinjs-utils": "^1.1.3",
+        "@ant-design/fast-color": "^2.0.6",
+        "@ant-design/icons": "^5.6.1",
+        "@ant-design/react-slick": "~1.1.2",
+        "@babel/runtime": "^7.26.0",
+        "@rc-component/color-picker": "~2.0.1",
+        "@rc-component/mutate-observer": "^1.1.0",
+        "@rc-component/qrcode": "~1.1.0",
+        "@rc-component/tour": "~1.15.1",
+        "@rc-component/trigger": "^2.3.0",
+        "classnames": "^2.5.1",
+        "copy-to-clipboard": "^3.3.3",
+        "dayjs": "^1.11.11",
+        "rc-cascader": "~3.34.0",
+        "rc-checkbox": "~3.5.0",
+        "rc-collapse": "~3.9.0",
+        "rc-dialog": "~9.6.0",
+        "rc-drawer": "~7.3.0",
+        "rc-dropdown": "~4.2.1",
+        "rc-field-form": "~2.7.1",
+        "rc-image": "~7.12.0",
+        "rc-input": "~1.8.0",
+        "rc-input-number": "~9.5.0",
+        "rc-mentions": "~2.20.0",
+        "rc-menu": "~9.16.1",
+        "rc-motion": "^2.9.5",
+        "rc-notification": "~5.6.4",
+        "rc-pagination": "~5.1.0",
+        "rc-picker": "~4.11.3",
+        "rc-progress": "~4.0.0",
+        "rc-rate": "~2.13.1",
+        "rc-resize-observer": "^1.4.3",
+        "rc-segmented": "~2.7.0",
+        "rc-select": "~14.16.8",
+        "rc-slider": "~11.1.9",
+        "rc-steps": "~6.0.1",
+        "rc-switch": "~4.1.0",
+        "rc-table": "~7.54.0",
+        "rc-tabs": "~15.7.0",
+        "rc-textarea": "~1.10.2",
+        "rc-tooltip": "~6.4.0",
+        "rc-tree": "~5.13.1",
+        "rc-tree-select": "~5.27.0",
+        "rc-upload": "~4.11.0",
+        "rc-util": "^5.44.4",
+        "scroll-into-view-if-needed": "^3.1.0",
+        "throttle-debounce": "^5.0.2"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/anymatch/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.5.2",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz",
+      "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "browserslist": "^4.28.4",
+        "caniuse-lite": "^1.0.30001799",
+        "fraction.js": "^5.3.4",
+        "picocolors": "^1.1.1",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
+      "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.40",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
+      "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "5.0.7",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+      "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.4",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
+      "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.38",
+        "caniuse-lite": "^1.0.30001799",
+        "electron-to-chromium": "^1.5.376",
+        "node-releases": "^2.0.48",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001800",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz",
+      "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/classnames": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+      "license": "MIT"
+    },
+    "node_modules/cli-cursor": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-truncate": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
+      "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "slice-ansi": "^8.0.0",
+        "string-width": "^8.2.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/colorette": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
+      "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/copy-to-clipboard": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+      "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+      "license": "MIT",
+      "dependencies": {
+        "toggle-selection": "^1.0.6"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.21",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
+      "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.383",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz",
+      "integrity": "sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/emoji-regex": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+      "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/environment": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+      "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
+      "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
+      "dev": true,
+      "license": "MIT",
+      "workspaces": [
+        "packages/*"
+      ],
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.8.0",
+        "@eslint-community/regexpp": "^4.12.2",
+        "@eslint/config-array": "^0.23.5",
+        "@eslint/config-helpers": "^0.6.0",
+        "@eslint/core": "^1.2.1",
+        "@eslint/plugin-kit": "^0.7.2",
+        "@humanfs/node": "^0.16.6",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.4.2",
+        "@types/estree": "^1.0.6",
+        "ajv": "^6.14.0",
+        "cross-spawn": "^7.0.6",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^9.1.2",
+        "eslint-visitor-keys": "^5.0.1",
+        "espree": "^11.2.0",
+        "esquery": "^1.7.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "minimatch": "^10.2.4",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-config-prettier": {
+      "version": "10.1.8",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+      "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint-config-prettier"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-react-hooks": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+      "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.24.4",
+        "@babel/parser": "^7.24.4",
+        "hermes-parser": "^0.25.1",
+        "zod": "^3.25.0 || ^4.0.0",
+        "zod-validation-error": "^3.5.0 || ^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-react-refresh": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz",
+      "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "eslint": "^9 || ^10"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "9.1.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+      "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@types/esrecurse": "^4.3.1",
+        "@types/estree": "^1.0.8",
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/eslint-visitor-keys": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+      "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/espree": {
+      "version": "11.2.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+      "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^5.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree/node_modules/eslint-visitor-keys": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+      "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+      "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+      "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fastq": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+      "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+      "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+      "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.4",
+        "mime-types": "^2.1.35"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+      "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+      "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hermes-estree": {
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+      "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/hermes-parser": {
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+      "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hermes-estree": "0.25.1"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/husky": {
+      "version": "9.1.7",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
+      "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "husky": "bin.js"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/typicode"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.2",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+      "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+      "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.3.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/jiti": {
+      "version": "1.21.7",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json2mq": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+      "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "string-convert": "^0.2.0"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lint-staged": {
+      "version": "16.4.0",
+      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz",
+      "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "commander": "^14.0.3",
+        "listr2": "^9.0.5",
+        "picomatch": "^4.0.3",
+        "string-argv": "^0.3.2",
+        "tinyexec": "^1.0.4",
+        "yaml": "^2.8.2"
+      },
+      "bin": {
+        "lint-staged": "bin/lint-staged.js"
+      },
+      "engines": {
+        "node": ">=20.17"
+      },
+      "funding": {
+        "url": "https://opencollective.com/lint-staged"
+      }
+    },
+    "node_modules/lint-staged/node_modules/commander": {
+      "version": "14.0.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+      "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/listr2": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
+      "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cli-truncate": "^5.0.0",
+        "colorette": "^2.0.20",
+        "eventemitter3": "^5.0.1",
+        "log-update": "^6.1.0",
+        "rfdc": "^1.4.1",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-update": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+      "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^7.0.0",
+        "cli-cursor": "^5.0.0",
+        "slice-ansi": "^7.1.0",
+        "strip-ansi": "^7.1.0",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-update/node_modules/slice-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+      "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "is-fullwidth-code-point": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.15",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+      "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.50",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
+      "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-function": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.16",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+      "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.12",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+      "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+      "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "lilconfig": "^3.1.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "jiti": ">=1.21.0",
+        "postcss": ">=8.0.9",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        },
+        "postcss": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "postcss-selector-parser": "^6.1.1"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
+      "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.9.4",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz",
+      "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/rc-cascader": {
+      "version": "3.34.0",
+      "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz",
+      "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.7",
+        "classnames": "^2.3.1",
+        "rc-select": "~14.16.2",
+        "rc-tree": "~5.13.0",
+        "rc-util": "^5.43.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-checkbox": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz",
+      "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.25.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-collapse": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz",
+      "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.3.4",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-dialog": {
+      "version": "9.6.0",
+      "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz",
+      "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/portal": "^1.0.0-8",
+        "classnames": "^2.2.6",
+        "rc-motion": "^2.3.0",
+        "rc-util": "^5.21.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-drawer": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz",
+      "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.9",
+        "@rc-component/portal": "^1.1.1",
+        "classnames": "^2.2.6",
+        "rc-motion": "^2.6.1",
+        "rc-util": "^5.38.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-dropdown": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz",
+      "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.44.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.11.0",
+        "react-dom": ">=16.11.0"
+      }
+    },
+    "node_modules/rc-field-form": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz",
+      "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "@rc-component/async-validator": "^5.0.3",
+        "rc-util": "^5.32.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-image": {
+      "version": "7.12.0",
+      "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz",
+      "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@rc-component/portal": "^1.0.2",
+        "classnames": "^2.2.6",
+        "rc-dialog": "~9.6.0",
+        "rc-motion": "^2.6.2",
+        "rc-util": "^5.34.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-input": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz",
+      "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.18.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/rc-input-number": {
+      "version": "9.5.0",
+      "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz",
+      "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/mini-decimal": "^1.0.1",
+        "classnames": "^2.2.5",
+        "rc-input": "~1.8.0",
+        "rc-util": "^5.40.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-mentions": {
+      "version": "2.20.0",
+      "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz",
+      "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.22.5",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.6",
+        "rc-input": "~1.8.0",
+        "rc-menu": "~9.16.0",
+        "rc-textarea": "~1.10.0",
+        "rc-util": "^5.34.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-menu": {
+      "version": "9.16.1",
+      "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz",
+      "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "2.x",
+        "rc-motion": "^2.4.3",
+        "rc-overflow": "^1.3.1",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-motion": {
+      "version": "2.9.5",
+      "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz",
+      "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.44.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-notification": {
+      "version": "5.6.4",
+      "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz",
+      "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.9.0",
+        "rc-util": "^5.20.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-overflow": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz",
+      "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.37.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-pagination": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz",
+      "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.38.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-picker": {
+      "version": "4.11.3",
+      "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz",
+      "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.1",
+        "rc-overflow": "^1.3.2",
+        "rc-resize-observer": "^1.4.0",
+        "rc-util": "^5.43.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "date-fns": ">= 2.x",
+        "dayjs": ">= 1.x",
+        "luxon": ">= 3.x",
+        "moment": ">= 2.x",
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      },
+      "peerDependenciesMeta": {
+        "date-fns": {
+          "optional": true
+        },
+        "dayjs": {
+          "optional": true
+        },
+        "luxon": {
+          "optional": true
+        },
+        "moment": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/rc-progress": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz",
+      "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.16.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-rate": {
+      "version": "2.13.1",
+      "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz",
+      "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-resize-observer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz",
+      "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.7",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.44.1",
+        "resize-observer-polyfill": "^1.5.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-segmented": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz",
+      "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-motion": "^2.4.4",
+        "rc-util": "^5.17.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/rc-select": {
+      "version": "14.16.8",
+      "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz",
+      "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/trigger": "^2.1.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.0.1",
+        "rc-overflow": "^1.3.1",
+        "rc-util": "^5.16.1",
+        "rc-virtual-list": "^3.5.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-slider": {
+      "version": "11.1.9",
+      "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz",
+      "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.36.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-steps": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz",
+      "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.16.7",
+        "classnames": "^2.2.3",
+        "rc-util": "^5.16.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-switch": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz",
+      "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.30.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-table": {
+      "version": "7.54.0",
+      "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz",
+      "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/context": "^1.4.0",
+        "classnames": "^2.2.5",
+        "rc-resize-observer": "^1.1.0",
+        "rc-util": "^5.44.3",
+        "rc-virtual-list": "^3.14.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tabs": {
+      "version": "15.7.0",
+      "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz",
+      "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "classnames": "2.x",
+        "rc-dropdown": "~4.2.0",
+        "rc-menu": "~9.16.0",
+        "rc-motion": "^2.6.2",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.34.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-textarea": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz",
+      "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.1",
+        "rc-input": "~1.8.0",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tooltip": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz",
+      "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.3.1",
+        "rc-util": "^5.44.3"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tree": {
+      "version": "5.13.1",
+      "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz",
+      "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.0.1",
+        "rc-util": "^5.16.1",
+        "rc-virtual-list": "^3.5.1"
+      },
+      "engines": {
+        "node": ">=10.x"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-tree-select": {
+      "version": "5.27.0",
+      "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz",
+      "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.7",
+        "classnames": "2.x",
+        "rc-select": "~14.16.2",
+        "rc-tree": "~5.13.0",
+        "rc-util": "^5.43.0"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-upload": {
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz",
+      "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.2.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-util": {
+      "version": "5.44.4",
+      "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz",
+      "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "react-is": "^18.2.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-virtual-list": {
+      "version": "3.19.2",
+      "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz",
+      "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.0",
+        "classnames": "^2.2.6",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.36.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/react": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+      "license": "MIT"
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-router": {
+      "version": "7.18.1",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
+      "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "^1.0.1",
+        "set-cookie-parser": "^2.6.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "7.18.1",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
+      "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
+      "license": "MIT",
+      "dependencies": {
+        "react-router": "7.18.1"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/readdirp/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
+    "node_modules/resolve": {
+      "version": "1.22.12",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+      "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "is-core-module": "^2.16.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+      "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.9"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.62.2",
+        "@rollup/rollup-android-arm64": "4.62.2",
+        "@rollup/rollup-darwin-arm64": "4.62.2",
+        "@rollup/rollup-darwin-x64": "4.62.2",
+        "@rollup/rollup-freebsd-arm64": "4.62.2",
+        "@rollup/rollup-freebsd-x64": "4.62.2",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+        "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+        "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+        "@rollup/rollup-linux-arm64-musl": "4.62.2",
+        "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+        "@rollup/rollup-linux-loong64-musl": "4.62.2",
+        "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+        "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+        "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+        "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+        "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+        "@rollup/rollup-linux-x64-gnu": "4.62.2",
+        "@rollup/rollup-linux-x64-musl": "4.62.2",
+        "@rollup/rollup-openbsd-x64": "4.62.2",
+        "@rollup/rollup-openharmony-arm64": "4.62.2",
+        "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+        "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+        "@rollup/rollup-win32-x64-gnu": "4.62.2",
+        "@rollup/rollup-win32-x64-msvc": "4.62.2",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+      "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^3.0.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/set-cookie-parser": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+      "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+      "license": "MIT"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/slice-ansi": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
+      "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.3",
+        "is-fullwidth-code-point": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-argv": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+      "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6.19"
+      }
+    },
+    "node_modules/string-convert": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+      "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
+      "license": "MIT"
+    },
+    "node_modules/string-width": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
+      "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.5.0",
+        "strip-ansi": "^7.1.2"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/stylis": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
+      "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+      "license": "MIT"
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.1",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+      "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "tinyglobby": "^0.2.11",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+      "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.6.0",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.2",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.7",
+        "lilconfig": "^3.1.3",
+        "micromatch": "^4.0.8",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
+    "node_modules/tinyexec": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+      "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toggle-selection": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+      "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
+      "license": "MIT"
+    },
+    "node_modules/ts-api-utils": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+      "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/typescript-eslint": {
+      "version": "8.62.1",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
+      "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "8.62.1",
+        "@typescript-eslint/parser": "8.62.1",
+        "@typescript-eslint/typescript-estree": "8.62.1",
+        "@typescript-eslint/utils": "8.62.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "6.4.3",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+      "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+      "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "string-width": "^7.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yaml": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+      "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/eemeli"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zod": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+      "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zod-validation-error": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+      "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "peerDependencies": {
+        "zod": "^3.25.0 || ^4.0.0"
+      }
+    },
+    "node_modules/zustand": {
+      "version": "5.0.14",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+      "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=18.0.0",
+        "immer": ">=9.0.6",
+        "react": ">=18.0.0",
+        "use-sync-external-store": ">=1.2.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "immer": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "use-sync-external-store": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..3c79ec2
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,55 @@
+{
+  "name": "rocketmq-studio-web",
+  "private": true,
+  "version": "0.1.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "preview": "vite preview",
+    "lint": "eslint .",
+    "lint:fix": "eslint . --fix",
+    "format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
+    "prepare": "cd .. && husky"
+  },
+  "lint-staged": {
+    "*.{ts,tsx}": [
+      "eslint --fix",
+      "prettier --write"
+    ],
+    "*.{css,json}": [
+      "prettier --write"
+    ]
+  },
+  "dependencies": {
+    "@ant-design/icons": "^5.5.0",
+    "@phosphor-icons/react": "^2.1.10",
+    "antd": "^5.22.0",
+    "axios": "^1.7.0",
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1",
+    "react-router-dom": "^7.0.0",
+    "zustand": "^5.0.0"
+  },
+  "devDependencies": {
+    "@eslint/js": "^10.0.1",
+    "@types/react": "^18.3.0",
+    "@types/react-dom": "^18.3.0",
+    "@typescript-eslint/eslint-plugin": "^8.62.1",
+    "@typescript-eslint/parser": "^8.62.1",
+    "@vitejs/plugin-react": "^4.3.0",
+    "autoprefixer": "^10.5.2",
+    "eslint": "^10.6.0",
+    "eslint-config-prettier": "^10.1.8",
+    "eslint-plugin-react-hooks": "^7.1.1",
+    "eslint-plugin-react-refresh": "^0.5.3",
+    "husky": "^9.1.7",
+    "lint-staged": "^16.4.0",
+    "postcss": "^8.5.16",
+    "prettier": "^3.9.4",
+    "tailwindcss": "^3.4.19",
+    "typescript": "^5.6.0",
+    "typescript-eslint": "^8.62.1",
+    "vite": "^6.0.0"
+  }
+}
diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/web/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}
diff --git a/web/src/App.tsx b/web/src/App.tsx
new file mode 100644
index 0000000..eac2795
--- /dev/null
+++ b/web/src/App.tsx
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Routes, Route, Navigate } from 'react-router-dom';
+import MainLayout from './layouts/MainLayout';
+import HomePage from './pages/home';
+import InstancePage from './pages/instance';
+import TopicPage from './pages/instance/topic';
+import ConsumerPage from './pages/instance/consumer';
+import MessagePage from './pages/instance/message';
+import AclPage from './pages/instance/acl';
+import DlqPage from './pages/instance/dlq';
+import ClusterPage from './pages/cluster';
+import K8sCertsPage from './pages/cluster/certs';
+import ClientsPage from './pages/cluster/clients';
+import DashboardOpsPage from './pages/home/dashboard';
+import AlertsPage from './pages/ops/alerts';
+import SystemAlertsPage from './pages/ops/systemAlerts';
+import AuditPage from './pages/ops/audit';
+import AiPage from './pages/ai';
+import SettingsPage from './pages/settings';
+
+function App() {
+  return (
+    <Routes>
+      <Route path="/" element={<MainLayout />}>
+        <Route index element={<HomePage />} />
+        <Route path="instance" element={<InstancePage />} />
+        <Route path="instance/topic" element={<TopicPage />} />
+        <Route path="instance/consumer" element={<ConsumerPage />} />
+        <Route path="instance/message" element={<MessagePage />} />
+        <Route path="instance/acl" element={<AclPage />} />
+        <Route path="instance/dlq" element={<DlqPage />} />
+        <Route path="cluster" element={<ClusterPage />} />
+        <Route path="cluster/certs" element={<K8sCertsPage />} />
+        <Route path="cluster/clients" element={<ClientsPage />} />
+        <Route path="ops/dashboard" element={<DashboardOpsPage />} />
+        <Route path="ops/alerts" element={<AlertsPage />} />
+        <Route path="ops/system-alerts" element={<SystemAlertsPage />} />
+        <Route path="ops/audit" element={<AuditPage />} />
+        <Route path="ai" element={<AiPage />} />
+        <Route path="settings" element={<SettingsPage />} />
+        <Route path="*" element={<Navigate to="/" replace />} />
+      </Route>
+    </Routes>
+  );
+}
+
+export default App;
diff --git a/web/src/config.ts b/web/src/config.ts
new file mode 100644
index 0000000..ec4e364
--- /dev/null
+++ b/web/src/config.ts
@@ -0,0 +1,7 @@
+/*
+ * Compile-time configuration.
+ * VITE_USE_MOCK controls whether the app uses mock data or real API calls.
+ * Set via .env file or build-time environment variable.
+ */
+
+export const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true';
diff --git a/web/src/constants/theme.ts b/web/src/constants/theme.ts
new file mode 100644
index 0000000..2cedb9b
--- /dev/null
+++ b/web/src/constants/theme.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 type { TagProps } from 'antd';
+
+export const THEME_COLORS = {
+  primary: '#1677ff',
+  success: '#52c41a',
+  warning: '#faad14',
+  error: '#ff4d4f',
+  info: '#1677ff',
+  purple: '#722ed1',
+  clusterV4: '#fa8c16',
+  clusterV5Local: '#1677ff',
+  clusterV5Cluster: '#722ed1',
+} as const;
+
+export const CLUSTER_TYPE_MAP: Record<string, { label: string; color: TagProps['color'] }> = {
+  V4_DIRECT: { label: 'V4 直连', color: 'orange' },
+  V5_PROXY_LOCAL: { label: 'V5 Proxy 单节点', color: 'blue' },
+  V5_PROXY_CLUSTER: { label: 'V5 Proxy 集群', color: 'purple' },
+};
+
+export const STATUS_MAP: Record<string, { label: string; color: string; dot: string }> = {
+  healthy: { label: '运行中', color: '#52c41a', dot: '#52c41a' },
+  warning: { label: '告警', color: '#faad14', dot: '#faad14' },
+  error: { label: '异常', color: '#ff4d4f', dot: '#ff4d4f' },
+  offline: { label: '离线', color: '#d9d9d9', dot: '#d9d9d9' },
+  connecting: { label: '连接中', color: '#1677ff', dot: '#1677ff' },
+};
+
+export const TOPIC_TYPE_MAP: Record<string, { label: string; color: TagProps['color'] }> = {
+  NORMAL: { label: '普通', color: 'default' },
+  FIFO: { label: '顺序', color: 'blue' },
+  DELAY: { label: '延迟', color: 'orange' },
+  TRANSACTION: { label: '事务', color: 'purple' },
+  LITE: { label: 'LiteTopic', color: 'magenta' },
+};
+
+export const PROTOCOL_MAP: Record<string, { label: string; color: TagProps['color'] }> = {
+  REMOTING: { label: 'Remoting', color: 'geekblue' },
+  GRPC: { label: 'gRPC', color: 'green' },
+};
diff --git a/web/src/hooks/useApi.ts b/web/src/hooks/useApi.ts
new file mode 100644
index 0000000..509ff89
--- /dev/null
+++ b/web/src/hooks/useApi.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState, useCallback } from 'react';
+
+function useApi<T>(apiFn: (...args: unknown[]) => Promise<T>) {
+  const [data, setData] = useState<T | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<Error | null>(null);
+
+  const execute = useCallback(
+    async (...args: unknown[]) => {
+      setLoading(true);
+      setError(null);
+      try {
+        const result = await apiFn(...args);
+        setData(result);
+        return result;
+      } catch (err) {
+        setError(err as Error);
+        throw err;
+      } finally {
+        setLoading(false);
+      }
+    },
+    [apiFn],
+  );
+
+  return { data, loading, error, execute };
+}
+
+export default useApi;
diff --git a/web/src/index.css b/web/src/index.css
new file mode 100644
index 0000000..c8c199f
--- /dev/null
+++ b/web/src/index.css
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* === CSS Variables === */
+:root {
+  --bolt-elements-bg-depth-1: #ffffff;
+  --bolt-elements-textPrimary: #1b1b1a;
+  --bolt-elements-textSecondary: #7c7670;
+  --bolt-elements-textTertiary: #a8a29e;
+  --oneday-home-accent: #0ea5e9;
+  --oneday-home-accent-tint: #bae6fd;
+  --oneday-home-fg: #1b1b1a;
+  --oneday-home-mute: #7c7670;
+  --oneday-home-motion-duration: 520ms;
+  --oneday-home-motion-ease: cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+/* === Keyframe Animations === */
+@keyframes oneday-orb-drift-a {
+  0% {
+    transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
+  }
+  25% {
+    transform: translate3d(35%, 18%, 0) scale(1.25) rotate(8deg);
+  }
+  50% {
+    transform: translate3d(48%, 38%, 0) scale(1.4) rotate(-4deg);
+  }
+  75% {
+    transform: translate3d(20%, 42%, 0) scale(1.1) rotate(6deg);
+  }
+  100% {
+    transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
+  }
+}
+
+@keyframes oneday-bg-drift {
+  0%,
+  100% {
+    filter: hue-rotate(0deg) saturate(1);
+  }
+  50% {
+    filter: hue-rotate(-18deg) saturate(1.15);
+  }
+}
+
+@keyframes wave {
+  0%,
+  100% {
+    transform: rotate(0deg);
+  }
+  25% {
+    transform: rotate(20deg);
+  }
+  50% {
+    transform: rotate(-10deg);
+  }
+  75% {
+    transform: rotate(15deg);
+  }
+}
+
+@keyframes float-in {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes pulse-ring {
+  0%,
+  100% {
+    box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.3);
+  }
+  50% {
+    box-shadow: 0 0 0 8px rgba(139, 92, 246, 0);
+  }
+}
+
+/* === Utility Classes === */
+.scrollbar-hide::-webkit-scrollbar {
+  display: none;
+}
+.scrollbar-hide {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+/* === Template Cards === */
+.template-card {
+  border-radius: 12px;
+  overflow: hidden;
+  cursor: pointer;
+  transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1);
+  border: 1px solid rgba(0, 0, 0, 0.06);
+  background: white;
+}
+.template-card:hover {
+  transform: translateY(-4px);
+  box-shadow: 0 12px 40px -12px rgba(80, 90, 180, 0.2);
+}
+
+/* === Tool Buttons === */
+.tool-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 12px;
+  border-radius: 9999px;
+  font-size: 0.893rem;
+  font-weight: 500;
+  background: #f9fafb;
+  color: #374151;
+  border: none;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.tool-btn:hover {
+  background: #f3f4f6;
+}
+
+/* === Mode Toggle === */
+.mode-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 16px;
+  font-size: 0.929rem;
+  font-weight: 500;
+  line-height: 1.5;
+  border-radius: 9999px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  color: #4b5563;
+  background: transparent;
+  border: none;
+}
+.mode-btn:hover {
+  background: rgba(255, 255, 255, 0.8);
+}
+.mode-btn.active {
+  color: #7c3aed;
+}
+
+/* === Chat Input === */
+.chat-input {
+  width: 100%;
+  padding: 16px 56px 16px 51px;
+  font-size: 14px;
+  line-height: 1.625;
+  min-height: 132px;
+  max-height: 300px;
+  resize: none;
+  border: none;
+  outline: none;
+  background: transparent;
+  color: var(--bolt-elements-textPrimary);
+  font-family: inherit;
+}
+.chat-input::placeholder {
+  color: var(--bolt-elements-textTertiary);
+}
diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx
new file mode 100644
index 0000000..b214bbe
--- /dev/null
+++ b/web/src/layouts/MainLayout.tsx
@@ -0,0 +1,400 @@
+/*
+ * 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 { useState } from 'react';
+import {
+  Layout,
+  Menu,
+  Breadcrumb,
+  Avatar,
+  Dropdown,
+  Input,
+  ConfigProvider,
+  theme,
+  Modal,
+} from 'antd';
+import { Outlet, useNavigate, useLocation } from 'react-router-dom';
+import {
+  House,
+  Database,
+  Monitor,
+  Sparkle,
+  GearSix,
+  ChatCircleText,
+  Key,
+  MagnifyingGlass,
+  ListDashes,
+  UserGear,
+  ChartBar,
+  Sun,
+  Moon,
+  ShieldCheck,
+  TrashSimple,
+  PlugsConnected,
+  BellRinging,
+  Notebook,
+} from '@phosphor-icons/react';
+import { useLang } from '../i18n/LangContext';
+
+const { Sider, Content } = Layout;
+
+const iconSize = 18;
+
+const MainLayout = () => {
+  const navigate = useNavigate();
+  const location = useLocation();
+  const [darkMode, setDarkMode] = useState(false);
+  const [searchOpen, setSearchOpen] = useState(false);
+  const [searchText, setSearchText] = useState('');
+  const { lang, setLang, t } = useLang();
+
+  const menuItems = [
+    { key: '/', icon: <House size={iconSize} weight="duotone" />, label: t('nav.home') },
+    {
+      key: 'instance-group',
+      icon: <Database size={iconSize} weight="duotone" />,
+      label: t('nav.instance'),
+      children: [
+        { key: '/instance', icon: <Database size={16} />, label: t('nav.instanceList') },
+        { key: '/instance/topic', icon: <ListDashes size={16} />, label: t('nav.topic') },
+        { key: '/instance/consumer', icon: <ChatCircleText size={16} />, label: t('nav.group') },
+        { key: '/instance/acl', icon: <Key size={16} />, label: t('nav.acl') },
+        { key: '/instance/message', icon: <MagnifyingGlass size={16} />, label: t('nav.message') },
+        { key: '/instance/dlq', icon: <TrashSimple size={16} />, label: t('nav.dlq') },
+      ],
+    },
+    {
+      key: 'cluster-ops-group',
+      icon: <Monitor size={iconSize} weight="duotone" />,
+      label: t('nav.clusterOps'),
+      children: [
+        { key: '/ops/dashboard', icon: <ChartBar size={16} />, label: t('nav.dashboard') },
+        { key: '/cluster/certs', icon: <ShieldCheck size={16} />, label: t('nav.certs') },
+        { key: '/cluster', icon: <Database size={16} />, label: t('nav.rocketmqCluster') },
+        { key: '/cluster/clients', icon: <PlugsConnected size={16} />, label: t('nav.clients') },
+        { key: '/ops/alerts', icon: <BellRinging size={16} />, label: t('nav.alertRules') },
+        { key: '/ops/system-alerts', icon: <BellRinging size={16} />, label: t('nav.alertEvents') },
+        { key: '/ops/audit', icon: <Notebook size={16} />, label: t('nav.audit') },
+      ],
+    },
+    { key: '/ai', icon: <Sparkle size={iconSize} weight="duotone" />, label: t('nav.ai') },
+    {
+      key: '/settings',
+      icon: <GearSix size={iconSize} weight="duotone" />,
+      label: t('nav.settings'),
+    },
+  ];
+
+  const breadcrumbMap: Record<string, string> = {
+    '/': t('nav.home'),
+    '/ops': t('nav.clusterOps'),
+    '/instance': t('nav.instanceList'),
+    '/instance/topic': t('nav.topic'),
+    '/instance/consumer': t('nav.group'),
+    '/instance/message': t('nav.message'),
+    '/instance/acl': t('nav.acl'),
+    '/instance/dlq': t('nav.dlq'),
+    '/cluster': t('nav.rocketmqCluster'),
+    '/cluster/certs': t('nav.certs'),
+    '/cluster/clients': t('nav.clients'),
+    '/ops/dashboard': t('nav.dashboard'),
+    '/ops/system-alerts': t('nav.alertEvents'),
+    '/ops/alerts': t('nav.alertRules'),
+    '/ops/audit': t('nav.audit'),
+    '/ai': t('nav.ai'),
+    '/settings': t('nav.settings'),
+  };
+
+  const pathSnippets = location.pathname.split('/').filter((i) => i);
+  const breadcrumbItems = [
+    {
+      title: (
+        <span onClick={() => navigate('/')} style={{ cursor: 'pointer' }}>
+          🏠
+        </span>
+      ),
+      key: 'home',
+    },
+    ...pathSnippets.map((_, index) => {
+      const path = '/' + pathSnippets.slice(0, index + 1).join('/');
+      return {
+        title: breadcrumbMap[path] || path,
+        key: path,
+      };
+    }),
+  ];
+
+  const userMenu = {
+    items: [
+      { key: 'profile', icon: <UserGear size={14} />, label: t('user.profile') },
+      { type: 'divider' as const },
+      { key: 'logout', label: t('user.logout'), danger: true },
+    ],
+  };
+
+  const borderColor = darkMode ? '#3a3a3e' : '#f0f0f0';
+  const siderBg = darkMode ? '#2a2a2e' : '#ffffff';
+  const topBarBg = darkMode ? 'rgba(42,42,46,0.85)' : 'rgba(255,255,255,0.7)';
+  const logoColor = darkMode ? '#e5e5e5' : '#1b1b1a';
+
+  return (
+    <ConfigProvider
+      theme={{
+        algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
+        token: darkMode
+          ? {
+              colorBgBase: '#2a2a2e',
+              colorBgContainer: '#323236',
+              colorBgElevated: '#3a3a3e',
+              colorBorder: '#3a3a3e',
+              colorBorderSecondary: '#333337',
+            }
+          : undefined,
+      }}
+    >
+      <Layout style={{ minHeight: '100vh' }}>
+        <Sider
+          theme={darkMode ? 'dark' : 'light'}
+          collapsible
+          width={220}
+          style={{
+            background: siderBg,
+            borderRight: `1px solid ${borderColor}`,
+            boxShadow: darkMode ? '2px 0 8px rgba(0,0,0,0.2)' : '2px 0 8px rgba(0,0,0,0.03)',
+          }}
+        >
+          {/* Logo */}
+          <div
+            style={{
+              height: 48,
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              gap: 8,
+              borderBottom: `1px solid ${borderColor}`,
+              fontSize: 15,
+              fontWeight: 600,
+              color: logoColor,
+              letterSpacing: '-0.01em',
+            }}
+          >
+            <span style={{ fontSize: 20 }}>🚀</span>
+            <span>RocketMQ Studio</span>
+          </div>
+
+          {/* Navigation Menu */}
+          <Menu
+            theme={darkMode ? 'dark' : 'light'}
+            mode="inline"
+            selectedKeys={[location.pathname]}
+            defaultOpenKeys={['instance-group', 'cluster-ops-group']}
+            items={menuItems}
+            onClick={({ key }) => navigate(key)}
+            style={{ borderRight: 'none', paddingTop: 8, background: 'transparent' }}
+          />
+        </Sider>
+
+        <Layout style={{ background: 'transparent' }}>
+          {/* Top bar */}
+          <div
+            style={{
+              height: 48,
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'space-between',
+              padding: '0 24px',
+              background: topBarBg,
+              backdropFilter: 'blur(8px)',
+              borderBottom: `1px solid ${borderColor}`,
+            }}
+          >
+            {/* Left: Breadcrumb */}
+            <Breadcrumb items={breadcrumbItems} style={{ fontSize: 13 }} />
+
+            {/* Right: Search + Lang + Theme + User */}
+            <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
+              {/* Search button */}
+              <div
+                onClick={() => setSearchOpen(true)}
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  gap: 6,
+                  padding: '4px 12px',
+                  borderRadius: 6,
+                  border: `1px solid ${borderColor}`,
+                  cursor: 'pointer',
+                  fontSize: 13,
+                  color: '#9CA3AF',
+                  minWidth: 160,
+                }}
+              >
+                <MagnifyingGlass size={14} />
+                <span>{t('common.search')}</span>
+                <span
+                  style={{
+                    marginLeft: 'auto',
+                    fontSize: 11,
+                    padding: '1px 6px',
+                    borderRadius: 4,
+                    background: darkMode ? '#333' : '#f5f5f5',
+                    border: `1px solid ${borderColor}`,
+                  }}
+                >
+                  ⌘K
+                </span>
+              </div>
+
+              {/* Language toggle */}
+              <div
+                onClick={() => setLang(lang === 'zh' ? 'en' : 'zh')}
+                style={{
+                  cursor: 'pointer',
+                  display: 'flex',
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  width: 28,
+                  height: 28,
+                  borderRadius: 6,
+                  fontSize: 13,
+                  fontWeight: 600,
+                  color: '#1677ff',
+                  transition: 'background 0.2s',
+                }}
+                title={lang === 'zh' ? 'Switch to English' : '切换到中文'}
+              >
+                {lang === 'zh' ? 'En' : '中'}
+              </div>
+
+              {/* Theme toggle */}
+              <div
+                onClick={() => setDarkMode(!darkMode)}
+                style={{
+                  cursor: 'pointer',
+                  display: 'flex',
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  width: 28,
+                  height: 28,
+                  borderRadius: 6,
+                  transition: 'background 0.2s',
+                }}
+                title={darkMode ? 'Light mode' : 'Dark mode'}
+              >
+                {darkMode ? (
+                  <Sun size={18} color="#9CA3AF" weight="fill" />
+                ) : (
+                  <Moon size={18} color="#9CA3AF" weight="fill" />
+                )}
+              </div>
+
+              {/* User avatar */}
+              <Dropdown menu={userMenu} trigger={['click']}>
+                <Avatar
+                  size={28}
+                  style={{ backgroundColor: '#1677ff', cursor: 'pointer' }}
+                  icon={<UserGear size={16} />}
+                />
+              </Dropdown>
+            </div>
+          </div>
+
+          <Content
+            style={{
+              padding: 0,
+              background: 'transparent',
+              minHeight: 280,
+              overflow: 'auto',
+            }}
+          >
+            <Outlet />
+          </Content>
+        </Layout>
+      </Layout>
+
+      {/* Search Modal */}
+      <Modal
+        open={searchOpen}
+        onCancel={() => {
+          setSearchOpen(false);
+          setSearchText('');
+        }}
+        footer={null}
+        closable={false}
+        styles={{ body: { padding: 0 } }}
+        width={520}
+        centered
+      >
+        <div style={{ padding: '16px 20px 8px' }}>
+          <Input
+            size="large"
+            placeholder={t('common.searchPlaceholder')}
+            prefix={<MagnifyingGlass size={18} color="#9CA3AF" />}
+            value={searchText}
+            onChange={(e) => setSearchText(e.target.value)}
+            autoFocus
+            allowClear
+            style={{ fontSize: 16 }}
+          />
+        </div>
+        <div style={{ maxHeight: 360, overflow: 'auto', padding: '8px 12px 12px' }}>
+          {menuItems
+            .flatMap((item) => {
+              if ('children' in item && item.children) return item.children;
+              return [item];
+            })
+            .filter((item) => {
+              if (!searchText) return true;
+              const label = String(item.label).toLowerCase();
+              return label.includes(searchText.toLowerCase());
+            })
+            .map((item) => (
+              <div
+                key={item.key}
+                onClick={() => {
+                  navigate(item.key as string);
+                  setSearchOpen(false);
+                  setSearchText('');
+                }}
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  gap: 10,
+                  padding: '10px 12px',
+                  borderRadius: 8,
+                  cursor: 'pointer',
+                  fontSize: 14,
+                  transition: 'background 0.15s',
+                }}
+                onMouseEnter={(e) => {
+                  e.currentTarget.style.background = darkMode ? '#1a1a1a' : '#f5f5f5';
+                }}
+                onMouseLeave={(e) => {
+                  e.currentTarget.style.background = 'transparent';
+                }}
+              >
+                {item.icon}
+                <span>{item.label}</span>
+              </div>
+            ))}
+        </div>
+      </Modal>
+    </ConfigProvider>
+  );
+};
+
+export default MainLayout;
diff --git a/web/src/main.tsx b/web/src/main.tsx
new file mode 100644
index 0000000..73c687e
--- /dev/null
+++ b/web/src/main.tsx
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import { ConfigProvider } from 'antd';
+import zhCN from 'antd/locale/zh_CN';
+import enUS from 'antd/locale/en_US';
+import { LangProvider, useLang } from './i18n/LangContext';
+import App from './App';
+import './index.css';
+
+const ThemedApp = () => {
+  const { lang } = useLang();
+
+  return (
+    <ConfigProvider
+      locale={lang === 'zh' ? zhCN : enUS}
+      theme={{
+        token: {
+          colorPrimary: '#1677ff',
+          borderRadius: 8,
+          fontFamily:
+            '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
+        },
+        components: {
+          Card: { borderRadiusLG: 12 },
+          Table: { borderRadius: 8 },
+          Button: { borderRadius: 8 },
+          Tag: { borderRadiusSM: 6 },
+        },
+      }}
+    >
+      <BrowserRouter>
+        <App />
+      </BrowserRouter>
+    </ConfigProvider>
+  );
+};
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+  <React.StrictMode>
+    <LangProvider>
+      <ThemedApp />
+    </LangProvider>
+  </React.StrictMode>,
+);
diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts
new file mode 100644
index 0000000..0949d93
--- /dev/null
+++ b/web/src/vite-env.d.ts
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/// <reference types="vite/client" />
diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs
new file mode 100644
index 0000000..62c6d9c
--- /dev/null
+++ b/web/tailwind.config.cjs
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+  theme: {
+    extend: {},
+  },
+  plugins: [],
+};
diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json
new file mode 100644
index 0000000..7341f24
--- /dev/null
+++ b/web/tsconfig.app.json
@@ -0,0 +1,32 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+    "jsx": "react-jsx",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+
+    /* Path aliases */
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": ["src"]
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}
diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json
new file mode 100644
index 0000000..7f84f73
--- /dev/null
+++ b/web/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2022",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["vite.config.ts"]
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..8aef668
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+  plugins: [react()],
+  server: {
+    port: 5173,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8888',
+        changeOrigin: true,
+      },
+    },
+  },
+});


(rocketmq-dashboard) 08/09: feat: implement backend ops and settings modules

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit 95f7661de34e33a1a8f52b8f302b2f4775cfe133
Author: lizhimins <70...@qq.com>
AuthorDate: Tue Jul 7 17:55:00 2026 +0800

    feat: implement backend ops and settings modules
    
    - Dashboard with statistics (DashboardController)
    - Alert rule management (AlertRuleController, SystemAlertController)
    - Audit log recording (AuditController)
    - AI assistant with SSE streaming (AiController)
    - System settings management (SettingsController)
    - Data source configuration and testing
    - In-memory repository implementations
    - Unit tests for all services and controllers
---
 .../com/rocketmq/studio/ops/ai/AiCommandDTO.java   |  37 ++++
 .../com/rocketmq/studio/ops/ai/AiController.java   |  52 +++++
 .../rocketmq/studio/ops/ai/AiExecuteResultVO.java  |  31 +++
 .../java/com/rocketmq/studio/ops/ai/AiService.java |  63 +++++++
 .../java/com/rocketmq/studio/ops/ai/AiToolVO.java  |  32 ++++
 .../java/com/rocketmq/studio/ops/ai/ChatDTO.java   |  33 ++++
 .../com/rocketmq/studio/ops/ai/LlmGateway.java     |  26 +++
 .../com/rocketmq/studio/ops/ai/LlmGatewayStub.java |  62 ++++++
 .../com/rocketmq/studio/ops/ai/McpServerImpl.java  |  64 +++++++
 .../rocketmq/studio/ops/ai/McpServerRegistry.java  |  25 +++
 .../rocketmq/studio/ops/alert/AlertRepository.java |  34 ++++
 .../studio/ops/alert/AlertRuleController.java      |  64 +++++++
 .../com/rocketmq/studio/ops/alert/AlertRuleVO.java |  42 +++++
 .../rocketmq/studio/ops/alert/AlertService.java    |  93 +++++++++
 .../studio/ops/alert/InMemoryAlertRepository.java  |  77 ++++++++
 .../studio/ops/alert/SystemAlertController.java    |  54 ++++++
 .../rocketmq/studio/ops/alert/SystemAlertVO.java   |  38 ++++
 .../rocketmq/studio/ops/audit/AuditController.java |  57 ++++++
 .../rocketmq/studio/ops/audit/AuditRecordVO.java   |  41 ++++
 .../rocketmq/studio/ops/audit/AuditRepository.java |  29 +++
 .../rocketmq/studio/ops/audit/AuditService.java    |  76 ++++++++
 .../studio/ops/audit/InMemoryAuditRepository.java  |  67 +++++++
 .../studio/ops/dashboard/ClusterOverviewVO.java    |  46 +++++
 .../studio/ops/dashboard/DashboardController.java  |  37 ++++
 .../studio/ops/dashboard/DashboardDataVO.java      |  34 ++++
 .../studio/ops/dashboard/DashboardProvider.java    |  23 +++
 .../ops/dashboard/DashboardProviderStub.java       |  99 ++++++++++
 .../studio/ops/dashboard/DashboardService.java     |  35 ++++
 .../studio/ops/dashboard/DashboardStatsVO.java     |  41 ++++
 .../studio/settings/DataSourceTestDTO.java         |  32 ++++
 .../studio/settings/DataSourceTestResultVO.java    |  31 +++
 .../com/rocketmq/studio/settings/DataSourceVO.java |  35 ++++
 .../studio/settings/GeneralSettingsVO.java         |  39 ++++
 .../settings/InMemorySettingsRepository.java       |  77 ++++++++
 .../studio/settings/SettingsController.java        |  73 +++++++
 .../studio/settings/SettingsRepository.java        |  36 ++++
 .../rocketmq/studio/settings/SettingsService.java  |  77 ++++++++
 .../com/rocketmq/studio/ops/ai/AiServiceTest.java  | 184 ++++++++++++++++++
 .../studio/ops/alert/AlertServiceTest.java         | 209 +++++++++++++++++++++
 .../studio/ops/audit/AuditServiceTest.java         | 205 ++++++++++++++++++++
 .../ops/dashboard/DashboardControllerTest.java     | 118 ++++++++++++
 .../studio/ops/dashboard/DashboardServiceTest.java | 113 +++++++++++
 .../studio/settings/SettingsControllerTest.java    | 196 +++++++++++++++++++
 .../studio/settings/SettingsServiceTest.java       | 174 +++++++++++++++++
 44 files changed, 3011 insertions(+)

diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiCommandDTO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiCommandDTO.java
new file mode 100644
index 0000000..b0a0f6c
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiCommandDTO.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.ops.ai;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiCommandDTO {
+    private String command;
+    private String mode;
+    private String model;
+    private String conversationId;
+    private String prompt;
+    private Map<String, Object> context;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiController.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiController.java
new file mode 100644
index 0000000..27ce146
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiController.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.ai;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/ai")
+@RequiredArgsConstructor
+public class AiController {
+
+    private final AiService aiService;
+
+    @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter chat(@RequestBody ChatDTO request) {
+        return aiService.chat(request);
+    }
+
+    @PostMapping("/execute")
+    public Result<AiExecuteResultVO> execute(@RequestBody AiCommandDTO command) {
+        return Result.ok(aiService.execute(command));
+    }
+
+    @GetMapping("/tools")
+    public Result<List<AiToolVO>> listTools() {
+        return Result.ok(aiService.listTools());
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiExecuteResultVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiExecuteResultVO.java
new file mode 100644
index 0000000..8f8cdcd
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiExecuteResultVO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.ops.ai;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiExecuteResultVO {
+    private boolean success;
+    private String result;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiService.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiService.java
new file mode 100644
index 0000000..b14479a
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiService.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.ai;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AiService {
+
+    private final LlmGateway llmGateway;
+    private final McpServerRegistry mcpServerRegistry;
+
+
+    public SseEmitter chat(ChatDTO request) {
+        log.info("Chat request received: mode={}, conversationId={}", request.getMode(), request.getConversationId());
+        return llmGateway.chat(request);
+    }
+
+
+    public AiExecuteResultVO execute(AiCommandDTO command) {
+        log.info("Executing AI command: {}", command.getCommand());
+        try {
+            String result = llmGateway.execute(command);
+            return AiExecuteResultVO.builder()
+                    .success(true)
+                    .result(result)
+                    .build();
+        } catch (Exception e) {
+            log.error("Failed to execute AI command", e);
+            return AiExecuteResultVO.builder()
+                    .success(false)
+                    .result("Error: " + e.getMessage())
+                    .build();
+        }
+    }
+
+
+    public List<AiToolVO> listTools() {
+        log.debug("Listing available AI tools");
+        return mcpServerRegistry.listTools();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiToolVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiToolVO.java
new file mode 100644
index 0000000..fc2dbe5
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiToolVO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.ops.ai;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiToolVO {
+    private String name;
+    private String description;
+    private Object parameters;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/ChatDTO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/ChatDTO.java
new file mode 100644
index 0000000..d3f0893
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/ChatDTO.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.rocketmq.studio.ops.ai;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChatDTO {
+    private String message;
+    private String mode;
+    private String model;
+    private String conversationId;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGateway.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGateway.java
new file mode 100644
index 0000000..257d970
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGateway.java
@@ -0,0 +1,26 @@
+/*
+ * 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 com.rocketmq.studio.ops.ai;
+
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+public interface LlmGateway {
+
+    SseEmitter chat(ChatDTO request);
+
+    String execute(AiCommandDTO command);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGatewayStub.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGatewayStub.java
new file mode 100644
index 0000000..97266f1
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGatewayStub.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.ai;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Slf4j
+@Component
+public class LlmGatewayStub implements LlmGateway {
+
+    private final ExecutorService executor = Executors.newCachedThreadPool();
+
+    @Override
+    public SseEmitter chat(ChatDTO request) {
+        SseEmitter emitter = new SseEmitter(60_000L);
+
+        executor.execute(() -> {
+            try {
+                String[] tokens = ("This is a stub response for: " + request.getMessage()).split(" ");
+                for (String token : tokens) {
+                    emitter.send(SseEmitter.event()
+                            .name("message")
+                            .data("{\"text\": \"" + token + " \"}"));
+                    Thread.sleep(100);
+                }
+                emitter.send(SseEmitter.event().name("done").data("[DONE]"));
+                emitter.complete();
+            } catch (IOException | InterruptedException e) {
+                log.error("Error streaming stub response", e);
+                emitter.completeWithError(e);
+            }
+        });
+
+        return emitter;
+    }
+
+    @Override
+    public String execute(AiCommandDTO command) {
+        log.info("Stub executing AI command: {}", command.getCommand());
+        return "Command executed successfully (stub)";
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerImpl.java b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerImpl.java
new file mode 100644
index 0000000..50aaa45
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerImpl.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.rocketmq.studio.ops.ai;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Component
+public class McpServerImpl implements McpServerRegistry {
+
+    @Override
+    public List<AiToolVO> listTools() {
+        log.debug("Listing available MCP tools (stub)");
+
+        Map<String, Object> queryParams = new HashMap<>();
+        queryParams.put("type", "object");
+        queryParams.put("properties", Collections.singletonMap("query",
+                Collections.singletonMap("type", "string")));
+
+        Map<String, Object> brokerParams = new HashMap<>();
+        brokerParams.put("type", "object");
+        brokerParams.put("properties", Collections.singletonMap("brokerName",
+                Collections.singletonMap("type", "string")));
+
+        return Arrays.asList(
+                AiToolVO.builder()
+                        .name("query_metrics")
+                        .description("Query RocketMQ metrics from Prometheus")
+                        .parameters(queryParams)
+                        .build(),
+                AiToolVO.builder()
+                        .name("list_brokers")
+                        .description("List all RocketMQ brokers in the cluster")
+                        .parameters(Collections.emptyMap())
+                        .build(),
+                AiToolVO.builder()
+                        .name("diagnose_broker")
+                        .description("Diagnose issues with a specific broker")
+                        .parameters(brokerParams)
+                        .build()
+        );
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerRegistry.java b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerRegistry.java
new file mode 100644
index 0000000..24b5fff
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerRegistry.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.ops.ai;
+
+
+import java.util.List;
+
+public interface McpServerRegistry {
+
+    List<AiToolVO> listTools();
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRepository.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRepository.java
new file mode 100644
index 0000000..958abc4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRepository.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+
+import java.util.List;
+
+public interface AlertRepository {
+    List<AlertRuleVO> findAllRules();
+
+    AlertRuleVO saveRule(AlertRuleVO rule);
+
+    void deleteRule(String id);
+
+    List<SystemAlertVO> findAlerts(String level);
+
+    SystemAlertVO saveAlert(SystemAlertVO alert);
+
+    int deleteAcknowledgedAlerts();
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleController.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleController.java
new file mode 100644
index 0000000..c8ea802
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleController.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/alert-rules")
+@RequiredArgsConstructor
+public class AlertRuleController {
+
+    private final AlertService alertService;
+
+    @GetMapping
+    public Result<List<AlertRuleVO>> listRules() {
+        return Result.ok(alertService.listRules());
+    }
+
+    @PostMapping("/create")
+    public Result<AlertRuleVO> createRule(@RequestBody AlertRuleVO rule) {
+        return Result.ok(alertService.createRule(rule));
+    }
+
+    @PostMapping("/update")
+    public Result<AlertRuleVO> updateRule(@RequestBody AlertRuleVO rule) {
+        return Result.ok(alertService.updateRule(rule));
+    }
+
+    @PostMapping("/toggle")
+    public Result<AlertRuleVO> toggleRule(@RequestBody Map<String, Object> request) {
+        String id = (String) request.get("id");
+        boolean enabled = (Boolean) request.get("enabled");
+        return Result.ok(alertService.toggleRule(id, enabled));
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> deleteRule(@RequestBody Map<String, String> request) {
+        alertService.deleteRule(request.get("id"));
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleVO.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleVO.java
new file mode 100644
index 0000000..377b457
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleVO.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AlertRuleVO {
+    private String id;
+    private String name;
+    private String metric;
+    private String operator;
+    private double threshold;
+    private String thresholdUnit;
+    private String duration;
+    private List<String> channels;
+    private boolean enabled;
+    private String lastTriggered;
+    private String description;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java
new file mode 100644
index 0000000..7336481
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java
@@ -0,0 +1,93 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AlertService {
+
+    private final AlertRepository alertRepository;
+
+
+    public List<AlertRuleVO> listRules() {
+        log.info("Listing all alert rules");
+        return alertRepository.findAllRules();
+    }
+
+
+    public AlertRuleVO createRule(AlertRuleVO rule) {
+        log.info("Creating alert rule: {}", rule.getName());
+        rule.setId(UUID.randomUUID().toString());
+        return alertRepository.saveRule(rule);
+    }
+
+
+    public AlertRuleVO updateRule(AlertRuleVO rule) {
+        log.info("Updating alert rule: {}", rule.getId());
+        return alertRepository.saveRule(rule);
+    }
+
+
+    public AlertRuleVO toggleRule(String id, boolean enabled) {
+        log.info("Toggling alert rule id={}, enabled={}", id, enabled);
+        List<AlertRuleVO> rules = alertRepository.findAllRules();
+        AlertRuleVO rule = rules.stream()
+                .filter(r -> r.getId().equals(id))
+                .findFirst()
+                .orElseThrow(() -> new com.rocketmq.studio.common.exception.BusinessException(404, "Alert rule not found: " + id));
+        rule.setEnabled(enabled);
+        return alertRepository.saveRule(rule);
+    }
+
+
+    public void deleteRule(String id) {
+        log.info("Deleting alert rule id={}", id);
+        alertRepository.deleteRule(id);
+    }
+
+
+    public List<SystemAlertVO> listAlerts(String level) {
+        log.info("Listing system alerts, level={}", level);
+        return alertRepository.findAlerts(level);
+    }
+
+
+    public SystemAlertVO acknowledgeAlert(String id) {
+        log.info("Acknowledging system alert id={}", id);
+        List<SystemAlertVO> alerts = alertRepository.findAlerts(null);
+        SystemAlertVO alert = alerts.stream()
+                .filter(a -> a.getId().equals(id))
+                .findFirst()
+                .orElseThrow(() -> new com.rocketmq.studio.common.exception.BusinessException(404, "System alert not found: " + id));
+        alert.setAcknowledged(true);
+        return alertRepository.saveAlert(alert);
+    }
+
+
+    public int clearAcknowledged() {
+        log.info("Clearing acknowledged system alerts");
+        return alertRepository.deleteAcknowledgedAlerts();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/InMemoryAlertRepository.java b/server/src/main/java/com/rocketmq/studio/ops/alert/InMemoryAlertRepository.java
new file mode 100644
index 0000000..d254728
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/InMemoryAlertRepository.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Component
+public class InMemoryAlertRepository implements AlertRepository {
+
+    private final Map<String, AlertRuleVO> rules = new ConcurrentHashMap<>();
+    private final Map<String, SystemAlertVO> alerts = new ConcurrentHashMap<>();
+
+    @Override
+    public List<AlertRuleVO> findAllRules() {
+        return new ArrayList<>(rules.values());
+    }
+
+    @Override
+    public AlertRuleVO saveRule(AlertRuleVO rule) {
+        rules.put(rule.getId(), rule);
+        log.debug("Saved alert rule id={}", rule.getId());
+        return rule;
+    }
+
+    @Override
+    public void deleteRule(String id) {
+        rules.remove(id);
+        log.debug("Deleted alert rule id={}", id);
+    }
+
+    @Override
+    public List<SystemAlertVO> findAlerts(String level) {
+        return alerts.values().stream()
+                .filter(a -> level == null || level.equalsIgnoreCase(a.getLevel().name()))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public SystemAlertVO saveAlert(SystemAlertVO alert) {
+        alerts.put(alert.getId(), alert);
+        log.debug("Saved system alert id={}", alert.getId());
+        return alert;
+    }
+
+    @Override
+    public int deleteAcknowledgedAlerts() {
+        List<String> toRemove = alerts.values().stream()
+                .filter(SystemAlertVO::isAcknowledged)
+                .map(SystemAlertVO::getId)
+                .collect(Collectors.toList());
+        toRemove.forEach(alerts::remove);
+        log.debug("Cleared {} acknowledged system alerts", toRemove.size());
+        return toRemove.size();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertController.java b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertController.java
new file mode 100644
index 0000000..2394944
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertController.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/system-alerts")
+@RequiredArgsConstructor
+public class SystemAlertController {
+
+    private final AlertService alertService;
+
+    @GetMapping
+    public Result<List<SystemAlertVO>> listAlerts(
+            @RequestParam(required = false) String level) {
+        return Result.ok(alertService.listAlerts(level));
+    }
+
+    @PostMapping("/acknowledge")
+    public Result<SystemAlertVO> acknowledgeAlert(@RequestBody Map<String, String> request) {
+        return Result.ok(alertService.acknowledgeAlert(request.get("id")));
+    }
+
+    @PostMapping("/clear-acknowledged")
+    public Result<Map<String, Integer>> clearAcknowledged() {
+        int cleared = alertService.clearAcknowledged();
+        return Result.ok(Map.of("cleared", cleared));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertVO.java b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertVO.java
new file mode 100644
index 0000000..9141e4e
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertVO.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.rocketmq.studio.ops.alert;
+
+import com.rocketmq.studio.common.domain.enums.AlertLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SystemAlertVO {
+    private String id;
+    private AlertLevel level;
+    private String title;
+    private String description;
+    private LocalDateTime time;
+    private boolean acknowledged;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditController.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditController.java
new file mode 100644
index 0000000..70b4292
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditController.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.rocketmq.studio.ops.audit;
+
+import com.rocketmq.studio.common.domain.PageResult;
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/audit-logs")
+@RequiredArgsConstructor
+public class AuditController {
+
+    private final AuditService auditService;
+
+    @GetMapping
+    public Result<PageResult<AuditRecordVO>> queryLogs(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int pageSize,
+            @RequestParam(required = false) String search,
+            @RequestParam(required = false) String operationType,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate,
+            @RequestParam(required = false) String result) {
+        return Result.ok(auditService.queryLogs(page, pageSize, search,
+                operationType, startDate, endDate, result));
+    }
+
+    @PostMapping("/cleanup")
+    public Result<Map<String, Integer>> cleanupLogs(@RequestBody Map<String, Integer> request) {
+        int beforeDays = request.getOrDefault("beforeDays", 30);
+        int deleted = auditService.cleanupLogs(beforeDays);
+        return Result.ok(Map.of("deleted", deleted));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRecordVO.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRecordVO.java
new file mode 100644
index 0000000..0f13abd
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRecordVO.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.audit;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class AuditRecordVO extends BaseEntity {
+    private LocalDateTime timestamp;
+    private String operator;
+    private String operationType;
+    private String target;
+    private String detail;
+    private String ipAddress;
+    private String result;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRepository.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRepository.java
new file mode 100644
index 0000000..6f12fe7
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRepository.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.rocketmq.studio.ops.audit;
+
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface AuditRepository {
+    List<AuditRecordVO> findAll(String search, String operationType,
+                              LocalDateTime startDate, LocalDateTime endDate,
+                              String result);
+
+    int deleteBefore(LocalDateTime cutoff);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditService.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditService.java
new file mode 100644
index 0000000..f5968b8
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditService.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.audit;
+
+import com.rocketmq.studio.common.domain.PageResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AuditService {
+
+    private final AuditRepository auditRepository;
+
+
+    public PageResult<AuditRecordVO> queryLogs(int page, int pageSize, String search,
+                                             String operationType, String startDate,
+                                             String endDate, String result) {
+        log.info("Querying audit logs, page={}, pageSize={}, search={}, operationType={}, result={}",
+                page, pageSize, search, operationType, result);
+
+        LocalDateTime start = parseDate(startDate, true);
+        LocalDateTime end = parseDate(endDate, false);
+
+        List<AuditRecordVO> allRecords = auditRepository.findAll(search, operationType, start, end, result);
+        long total = allRecords.size();
+
+        int fromIndex = Math.min((page - 1) * pageSize, allRecords.size());
+        int toIndex = Math.min(fromIndex + pageSize, allRecords.size());
+        List<AuditRecordVO> pageRecords = allRecords.subList(fromIndex, toIndex);
+
+        return PageResult.of(pageRecords, total, page, pageSize);
+    }
+
+
+    public int cleanupLogs(int beforeDays) {
+        log.info("Cleaning up audit logs older than {} days", beforeDays);
+        LocalDateTime cutoff = LocalDateTime.now().minusDays(beforeDays);
+        return auditRepository.deleteBefore(cutoff);
+    }
+
+    private LocalDateTime parseDate(String dateStr, boolean startOfDay) {
+        if (dateStr == null || dateStr.isEmpty()) {
+            return null;
+        }
+        try {
+            LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE);
+            return startOfDay ? date.atStartOfDay() : date.atTime(LocalTime.MAX);
+        } catch (Exception e) {
+            log.warn("Failed to parse date: {}", dateStr, e);
+            return null;
+        }
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/InMemoryAuditRepository.java b/server/src/main/java/com/rocketmq/studio/ops/audit/InMemoryAuditRepository.java
new file mode 100644
index 0000000..f4c2955
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/audit/InMemoryAuditRepository.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.audit;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Component
+public class InMemoryAuditRepository implements AuditRepository {
+
+    private final Map<String, AuditRecordVO> records = new ConcurrentHashMap<>();
+
+    @Override
+    public List<AuditRecordVO> findAll(String search, String operationType,
+                                     LocalDateTime startDate, LocalDateTime endDate,
+                                     String result) {
+        return records.values().stream()
+                .filter(r -> search == null || search.isEmpty()
+                        || r.getDetail().toLowerCase().contains(search.toLowerCase())
+                        || r.getOperator().toLowerCase().contains(search.toLowerCase())
+                        || r.getTarget().toLowerCase().contains(search.toLowerCase()))
+                .filter(r -> operationType == null || operationType.isEmpty()
+                        || operationType.equals(r.getOperationType()))
+                .filter(r -> startDate == null || r.getTimestamp() != null && !r.getTimestamp().isBefore(startDate))
+                .filter(r -> endDate == null || r.getTimestamp() != null && !r.getTimestamp().isAfter(endDate))
+                .filter(r -> result == null || result.isEmpty() || result.equals(r.getResult()))
+                .sorted((a, b) -> {
+                    if (a.getTimestamp() == null || b.getTimestamp() == null) {
+                        return 0;
+                    }
+                    return b.getTimestamp().compareTo(a.getTimestamp());
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public int deleteBefore(LocalDateTime cutoff) {
+        List<String> toRemove = records.values().stream()
+                .filter(r -> r.getTimestamp() != null && r.getTimestamp().isBefore(cutoff))
+                .map(AuditRecordVO::getId)
+                .collect(Collectors.toList());
+        toRemove.forEach(records::remove);
+        log.debug("Deleted {} audit records before {}", toRemove.size(), cutoff);
+        return toRemove.size();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java
new file mode 100644
index 0000000..9a20a86
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.ops.dashboard;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ClusterOverviewVO {
+    private String id;
+    private String name;
+    private ClusterType type;
+    private ClusterStatus status;
+    private int brokers;
+    private int proxies;
+    private int topics;
+    private int groups;
+    private int tpsIn;
+    private int tpsOut;
+    private String version;
+    private List<Integer> throughput;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardController.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardController.java
new file mode 100644
index 0000000..9f8f67e
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardController.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.ops.dashboard;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/dashboard")
+@RequiredArgsConstructor
+public class DashboardController {
+
+    private final DashboardService dashboardService;
+
+    @GetMapping
+    public Result<DashboardDataVO> getDashboard() {
+        return Result.ok(dashboardService.getDashboard());
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardDataVO.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardDataVO.java
new file mode 100644
index 0000000..f3bf8fe
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardDataVO.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.rocketmq.studio.ops.dashboard;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DashboardDataVO {
+    private DashboardStatsVO stats;
+    private List<ClusterOverviewVO> clusters;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProvider.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProvider.java
new file mode 100644
index 0000000..0464a37
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProvider.java
@@ -0,0 +1,23 @@
+/*
+ * 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 com.rocketmq.studio.ops.dashboard;
+
+
+public interface DashboardProvider {
+    DashboardDataVO getDashboardData();
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProviderStub.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
new file mode 100644
index 0000000..3964ed9
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProviderStub.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.ops.dashboard;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+public class DashboardProviderStub implements DashboardProvider {
+
+    @Override
+    public DashboardDataVO getDashboardData() {
+        DashboardStatsVO stats = DashboardStatsVO.builder()
+                .totalClusters(3)
+                .healthyClusters(2)
+                .totalBrokers(12)
+                .totalProxies(6)
+                .totalNameServers(6)
+                .totalTopics(156)
+                .totalConsumerGroups(89)
+                .totalMessagesToday(12_500_000L)
+                .messagesPerSecond(14_467L)
+                .tpsIn(7_200L)
+                .tpsOut(7_267L)
+                .build();
+
+        List<ClusterOverviewVO> clusters = List.of(
+                ClusterOverviewVO.builder()
+                        .id("cluster-1")
+                        .name("production-cluster")
+                        .type(ClusterType.V5_PROXY_CLUSTER)
+                        .status(ClusterStatus.healthy)
+                        .brokers(6)
+                        .proxies(3)
+                        .topics(80)
+                        .groups(45)
+                        .tpsIn(4200)
+                        .tpsOut(4300)
+                        .version("5.1.4")
+                        .throughput(Arrays.asList(3200, 3500, 4100, 4200, 3800, 4000,
+                                4500, 4200, 3900, 4100, 4300, 4200))
+                        .build(),
+                ClusterOverviewVO.builder()
+                        .id("cluster-2")
+                        .name("staging-cluster")
+                        .type(ClusterType.V5_PROXY_LOCAL)
+                        .status(ClusterStatus.warning)
+                        .brokers(4)
+                        .proxies(2)
+                        .topics(50)
+                        .groups(30)
+                        .tpsIn(2000)
+                        .tpsOut(1967)
+                        .version("5.1.4")
+                        .throughput(Arrays.asList(1800, 1900, 2100, 2000, 1700, 2200,
+                                2000, 1800, 1900, 2100, 2000, 1967))
+                        .build(),
+                ClusterOverviewVO.builder()
+                        .id("cluster-3")
+                        .name("dev-cluster")
+                        .type(ClusterType.V4_DIRECT)
+                        .status(ClusterStatus.healthy)
+                        .brokers(2)
+                        .proxies(1)
+                        .topics(26)
+                        .groups(14)
+                        .tpsIn(1000)
+                        .tpsOut(1000)
+                        .version("4.9.8")
+                        .throughput(Arrays.asList(800, 900, 1100, 1000, 950, 1050,
+                                1000, 900, 1100, 1000, 950, 1000))
+                        .build()
+        );
+
+        return DashboardDataVO.builder()
+                .stats(stats)
+                .clusters(clusters)
+                .build();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardService.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardService.java
new file mode 100644
index 0000000..9924b45
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardService.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.ops.dashboard;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DashboardService {
+
+    private final DashboardProvider dashboardProvider;
+
+    public DashboardDataVO getDashboard() {
+        log.debug("Fetching dashboard data");
+        return dashboardProvider.getDashboardData();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardStatsVO.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardStatsVO.java
new file mode 100644
index 0000000..cd22539
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardStatsVO.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.ops.dashboard;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DashboardStatsVO {
+    private int totalClusters;
+    private int healthyClusters;
+    private int totalBrokers;
+    private int totalProxies;
+    private int totalNameServers;
+    private int totalTopics;
+    private int totalConsumerGroups;
+    private long totalMessagesToday;
+    private long messagesPerSecond;
+    private long tpsIn;
+    private long tpsOut;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestDTO.java b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestDTO.java
new file mode 100644
index 0000000..b17b8dc
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestDTO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DataSourceTestDTO {
+    private String url;
+    private String type;
+    private String auth;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestResultVO.java b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestResultVO.java
new file mode 100644
index 0000000..f842fae
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestResultVO.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DataSourceTestResultVO {
+    private boolean success;
+    private String message;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/DataSourceVO.java b/server/src/main/java/com/rocketmq/studio/settings/DataSourceVO.java
new file mode 100644
index 0000000..c2d1164
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/DataSourceVO.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DataSourceVO {
+    private String key;
+    private String name;
+    private String type;
+    private String url;
+    private String auth;
+    private String status;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java
new file mode 100644
index 0000000..8dd3bd6
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class GeneralSettingsVO {
+    private String theme;
+    private boolean compact;
+    private boolean desktopNotify;
+    private boolean notifySound;
+    private int sessionTimeout;
+    private boolean requireLogin;
+    private String llmProvider;
+    private String apiKey;
+    private String model;
+    private String baseUrl;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/InMemorySettingsRepository.java b/server/src/main/java/com/rocketmq/studio/settings/InMemorySettingsRepository.java
new file mode 100644
index 0000000..d3c01e4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/InMemorySettingsRepository.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Component
+public class InMemorySettingsRepository implements SettingsRepository {
+
+    private GeneralSettingsVO generalSettings = GeneralSettingsVO.builder()
+            .theme("system")
+            .compact(false)
+            .desktopNotify(true)
+            .notifySound(false)
+            .sessionTimeout(30)
+            .requireLogin(false)
+            .llmProvider("openai")
+            .apiKey("")
+            .model("gpt-4")
+            .baseUrl("")
+            .build();
+
+    private final Map<String, DataSourceVO> dataSources = new ConcurrentHashMap<>();
+
+    @Override
+    public GeneralSettingsVO loadGeneralSettings() {
+        return generalSettings;
+    }
+
+    @Override
+    public void saveGeneralSettings(GeneralSettingsVO settings) {
+        this.generalSettings = settings;
+    }
+
+    @Override
+    public List<DataSourceVO> findAllDataSources() {
+        return new ArrayList<>(dataSources.values());
+    }
+
+    @Override
+    public DataSourceVO saveDataSource(DataSourceVO dataSource) {
+        dataSources.put(dataSource.getKey(), dataSource);
+        return dataSource;
+    }
+
+    @Override
+    public void deleteDataSource(String key) {
+        dataSources.remove(key);
+    }
+
+    @Override
+    public Optional<DataSourceVO> findDataSourceByKey(String key) {
+        return Optional.ofNullable(dataSources.get(key));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java b/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java
new file mode 100644
index 0000000..a9a9123
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.settings;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/settings")
+@RequiredArgsConstructor
+public class SettingsController {
+
+    private final SettingsService settingsService;
+
+    @GetMapping("/general")
+    public Result<GeneralSettingsVO> getGeneralSettings() {
+        return Result.ok(settingsService.getGeneralSettings());
+    }
+
+    @PostMapping("/general/save")
+    public Result<Void> saveGeneralSettings(@RequestBody GeneralSettingsVO settings) {
+        settingsService.saveGeneralSettings(settings);
+        return Result.ok();
+    }
+
+    @GetMapping("/datasources")
+    public Result<List<DataSourceVO>> listDataSources() {
+        return Result.ok(settingsService.listDataSources());
+    }
+
+    @PostMapping("/datasources/create")
+    public Result<DataSourceVO> createDataSource(@RequestBody DataSourceVO dataSource) {
+        return Result.ok(settingsService.createDataSource(dataSource));
+    }
+
+    @PostMapping("/datasources/update")
+    public Result<DataSourceVO> updateDataSource(@RequestBody DataSourceVO dataSource) {
+        return Result.ok(settingsService.updateDataSource(dataSource));
+    }
+
+    @PostMapping("/datasources/delete")
+    public Result<Void> deleteDataSource(@RequestParam String key) {
+        settingsService.deleteDataSource(key);
+        return Result.ok();
+    }
+
+    @PostMapping("/datasources/test")
+    public Result<DataSourceTestResultVO> testDataSource(@RequestBody DataSourceTestDTO request) {
+        return Result.ok(settingsService.testDataSource(request));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/SettingsRepository.java b/server/src/main/java/com/rocketmq/studio/settings/SettingsRepository.java
new file mode 100644
index 0000000..70e08a7
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsRepository.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+
+import java.util.List;
+import java.util.Optional;
+
+public interface SettingsRepository {
+
+    GeneralSettingsVO loadGeneralSettings();
+
+    void saveGeneralSettings(GeneralSettingsVO settings);
+
+    List<DataSourceVO> findAllDataSources();
+
+    DataSourceVO saveDataSource(DataSourceVO dataSource);
+
+    void deleteDataSource(String key);
+
+    Optional<DataSourceVO> findDataSourceByKey(String key);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java b/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java
new file mode 100644
index 0000000..1f12876
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SettingsService {
+
+    private final SettingsRepository settingsRepository;
+
+
+    public GeneralSettingsVO getGeneralSettings() {
+        log.debug("Loading general settings");
+        return settingsRepository.loadGeneralSettings();
+    }
+
+
+    public void saveGeneralSettings(GeneralSettingsVO settings) {
+        log.info("Saving general settings");
+        settingsRepository.saveGeneralSettings(settings);
+    }
+
+
+    public List<DataSourceVO> listDataSources() {
+        log.debug("Listing all data sources");
+        return settingsRepository.findAllDataSources();
+    }
+
+
+    public DataSourceVO createDataSource(DataSourceVO dataSource) {
+        log.info("Creating data source: {}", dataSource.getName());
+        return settingsRepository.saveDataSource(dataSource);
+    }
+
+
+    public DataSourceVO updateDataSource(DataSourceVO dataSource) {
+        log.info("Updating data source: {}", dataSource.getKey());
+        return settingsRepository.saveDataSource(dataSource);
+    }
+
+
+    public void deleteDataSource(String key) {
+        log.info("Deleting data source: {}", key);
+        settingsRepository.deleteDataSource(key);
+    }
+
+
+    public DataSourceTestResultVO testDataSource(DataSourceTestDTO request) {
+        log.info("Testing data source connection: url={}, type={}", request.getUrl(), request.getType());
+        // Stub: always return success for now
+        return DataSourceTestResultVO.builder()
+                .success(true)
+                .message("Connection successful")
+                .build();
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/ops/ai/AiServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/ai/AiServiceTest.java
new file mode 100644
index 0000000..ac46c7d
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/ops/ai/AiServiceTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.ai;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AiServiceTest {
+
+    @Mock
+    private LlmGateway llmGateway;
+
+    @Mock
+    private McpServerRegistry mcpServerRegistry;
+
+    @InjectMocks
+    private AiService aiService;
+
+    @Test
+    void chatShouldReturnSseEmitterFromGateway() {
+        ChatDTO request = ChatDTO.builder()
+                .message("What is the broker status?")
+                .mode("chat")
+                .model("gpt-4")
+                .conversationId("conv-1")
+                .build();
+        SseEmitter mockEmitter = new SseEmitter();
+        when(llmGateway.chat(request)).thenReturn(mockEmitter);
+
+        SseEmitter result = aiService.chat(request);
+
+        assertThat(result).isSameAs(mockEmitter);
+        verify(llmGateway).chat(request);
+    }
+
+    @Test
+    void chatShouldPassRequestDirectlyToGateway() {
+        ChatDTO request = ChatDTO.builder()
+                .message("List all topics")
+                .mode("agent")
+                .build();
+        SseEmitter mockEmitter = new SseEmitter();
+        when(llmGateway.chat(any(ChatDTO.class))).thenReturn(mockEmitter);
+
+        aiService.chat(request);
+
+        verify(llmGateway).chat(request);
+    }
+
+    @Test
+    void executeShouldReturnSuccessResult() {
+        AiCommandDTO command = AiCommandDTO.builder()
+                .command("list_topics")
+                .mode("agent")
+                .model("gpt-4")
+                .prompt("List all topics in the cluster")
+                .build();
+        when(llmGateway.execute(command)).thenReturn("Found 5 topics: topic-a, topic-b, topic-c, topic-d, topic-e");
+
+        AiExecuteResultVO result = aiService.execute(command);
+
+        assertThat(result.isSuccess()).isTrue();
+        assertThat(result.getResult()).contains("Found 5 topics");
+        verify(llmGateway).execute(command);
+    }
+
+    @Test
+    void executeShouldReturnFailureWhenGatewayThrows() {
+        AiCommandDTO command = AiCommandDTO.builder()
+                .command("delete_topic")
+                .mode("agent")
+                .prompt("Delete topic-x")
+                .build();
+        when(llmGateway.execute(command)).thenThrow(new RuntimeException("Permission denied"));
+
+        AiExecuteResultVO result = aiService.execute(command);
+
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getResult()).contains("Error: Permission denied");
+    }
+
+    @Test
+    void executeShouldHandleNullMessageInException() {
+        AiCommandDTO command = AiCommandDTO.builder().command("bad_cmd").build();
+        when(llmGateway.execute(command)).thenThrow(new RuntimeException());
+
+        AiExecuteResultVO result = aiService.execute(command);
+
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getResult()).startsWith("Error:");
+    }
+
+    @Test
+    void executeShouldPassContextToGateway() {
+        Map<String, Object> context = new HashMap<>();
+        context.put("clusterId", "cluster-1");
+        context.put("namespace", "default");
+        AiCommandDTO command = AiCommandDTO.builder()
+                .command("query_metrics")
+                .mode("agent")
+                .context(context)
+                .build();
+        when(llmGateway.execute(command)).thenReturn("CPU: 45%, Memory: 72%");
+
+        AiExecuteResultVO result = aiService.execute(command);
+
+        assertThat(result.isSuccess()).isTrue();
+        assertThat(result.getResult()).contains("CPU: 45%");
+    }
+
+    @Test
+    void listToolsShouldReturnAllTools() {
+        AiToolVO tool1 = AiToolVO.builder().name("list_topics").description("List all topics").build();
+        AiToolVO tool2 = AiToolVO.builder().name("query_metrics").description("Query cluster metrics").build();
+        AiToolVO tool3 = AiToolVO.builder().name("send_message").description("Send a test message").build();
+        when(mcpServerRegistry.listTools()).thenReturn(Arrays.asList(tool1, tool2, tool3));
+
+        List<AiToolVO> result = aiService.listTools();
+
+        assertThat(result).hasSize(3);
+        assertThat(result.get(0).getName()).isEqualTo("list_topics");
+        assertThat(result.get(0).getDescription()).isEqualTo("List all topics");
+        assertThat(result.get(1).getName()).isEqualTo("query_metrics");
+        assertThat(result.get(2).getName()).isEqualTo("send_message");
+    }
+
+    @Test
+    void listToolsShouldReturnEmptyListWhenNoTools() {
+        when(mcpServerRegistry.listTools()).thenReturn(Collections.emptyList());
+
+        List<AiToolVO> result = aiService.listTools();
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void listToolsShouldIncludeParameters() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("clusterId", "string");
+        params.put("limit", "integer");
+        AiToolVO tool = AiToolVO.builder()
+                .name("list_topics")
+                .description("List all topics")
+                .parameters(params)
+                .build();
+        when(mcpServerRegistry.listTools()).thenReturn(List.of(tool));
+
+        List<AiToolVO> result = aiService.listTools();
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getParameters()).isNotNull();
+        assertThat(result.get(0).getParameters()).isInstanceOf(Map.class);
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java
new file mode 100644
index 0000000..2fd408c
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.ops.alert;
+
+import com.rocketmq.studio.common.domain.enums.AlertLevel;
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AlertServiceTest {
+
+    @Mock
+    private AlertRepository alertRepository;
+
+    @InjectMocks
+    private AlertService alertService;
+
+    @Test
+    void listRulesShouldReturnAllRules() {
+        AlertRuleVO rule1 = AlertRuleVO.builder().id("1").name("High CPU").metric("cpu_usage")
+                .operator(">").threshold(90.0).enabled(true).build();
+        AlertRuleVO rule2 = AlertRuleVO.builder().id("2").name("Low Disk").metric("disk_free")
+                .operator("<").threshold(10.0).enabled(false).build();
+        when(alertRepository.findAllRules()).thenReturn(Arrays.asList(rule1, rule2));
+
+        List<AlertRuleVO> result = alertService.listRules();
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getName()).isEqualTo("High CPU");
+        assertThat(result.get(0).isEnabled()).isTrue();
+        assertThat(result.get(1).getName()).isEqualTo("Low Disk");
+        assertThat(result.get(1).isEnabled()).isFalse();
+    }
+
+    @Test
+    void listRulesShouldReturnEmptyListWhenNoRules() {
+        when(alertRepository.findAllRules()).thenReturn(Collections.emptyList());
+
+        List<AlertRuleVO> result = alertService.listRules();
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void createRuleShouldAssignId() {
+        AlertRuleVO input = AlertRuleVO.builder().name("New Rule").metric("tps")
+                .operator(">").threshold(1000.0).build();
+        when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        AlertRuleVO result = alertService.createRule(input);
+
+        assertThat(result.getId()).isNotNull().isNotEmpty();
+        assertThat(result.getName()).isEqualTo("New Rule");
+        assertThat(result.getMetric()).isEqualTo("tps");
+        verify(alertRepository).saveRule(result);
+    }
+
+    @Test
+    void createRuleShouldGenerateUniqueIds() {
+        AlertRuleVO input1 = AlertRuleVO.builder().name("Rule 1").build();
+        AlertRuleVO input2 = AlertRuleVO.builder().name("Rule 2").build();
+        when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        AlertRuleVO result1 = alertService.createRule(input1);
+        AlertRuleVO result2 = alertService.createRule(input2);
+
+        assertThat(result1.getId()).isNotEqualTo(result2.getId());
+    }
+
+    @Test
+    void toggleRuleShouldEnableRule() {
+        AlertRuleVO existing = AlertRuleVO.builder().id("rule-1").name("CPU Alert").enabled(false).build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(existing));
+        when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        AlertRuleVO result = alertService.toggleRule("rule-1", true);
+
+        assertThat(result.isEnabled()).isTrue();
+        verify(alertRepository).saveRule(result);
+    }
+
+    @Test
+    void toggleRuleShouldDisableRule() {
+        AlertRuleVO existing = AlertRuleVO.builder().id("rule-1").name("CPU Alert").enabled(true).build();
+        when(alertRepository.findAllRules()).thenReturn(List.of(existing));
+        when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        AlertRuleVO result = alertService.toggleRule("rule-1", false);
+
+        assertThat(result.isEnabled()).isFalse();
+    }
+
+    @Test
+    void toggleRuleShouldThrowWhenRuleNotFound() {
+        when(alertRepository.findAllRules()).thenReturn(Collections.emptyList());
+
+        assertThatThrownBy(() -> alertService.toggleRule("non-existent", true))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Alert rule not found: non-existent");
+    }
+
+    @Test
+    void deleteRuleShouldCallRepository() {
+        doNothing().when(alertRepository).deleteRule("rule-1");
+
+        alertService.deleteRule("rule-1");
+
+        verify(alertRepository).deleteRule("rule-1");
+    }
+
+    @Test
+    void listAlertsShouldReturnAlertsForLevel() {
+        SystemAlertVO alert1 = SystemAlertVO.builder().id("a1").level(AlertLevel.error)
+                .title("Broker Down").acknowledged(false).build();
+        SystemAlertVO alert2 = SystemAlertVO.builder().id("a2").level(AlertLevel.error)
+                .title("High Latency").acknowledged(false).build();
+        when(alertRepository.findAlerts("error")).thenReturn(Arrays.asList(alert1, alert2));
+
+        List<SystemAlertVO> result = alertService.listAlerts("error");
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getLevel()).isEqualTo(AlertLevel.error);
+        assertThat(result.get(0).getTitle()).isEqualTo("Broker Down");
+        verify(alertRepository).findAlerts("error");
+    }
+
+    @Test
+    void listAlertsShouldReturnAllAlertsWhenLevelIsNull() {
+        SystemAlertVO alert = SystemAlertVO.builder().id("a1").level(AlertLevel.warning)
+                .title("Slow Consumer").build();
+        when(alertRepository.findAlerts(null)).thenReturn(List.of(alert));
+
+        List<SystemAlertVO> result = alertService.listAlerts(null);
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getLevel()).isEqualTo(AlertLevel.warning);
+        verify(alertRepository).findAlerts(null);
+    }
+
+    @Test
+    void acknowledgeAlertShouldSetAcknowledgedTrue() {
+        SystemAlertVO existing = SystemAlertVO.builder().id("a1").level(AlertLevel.error)
+                .title("Broker Down").acknowledged(false).build();
+        when(alertRepository.findAlerts(null)).thenReturn(List.of(existing));
+        when(alertRepository.saveAlert(any(SystemAlertVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
+
+        SystemAlertVO result = alertService.acknowledgeAlert("a1");
+
+        assertThat(result.isAcknowledged()).isTrue();
+        verify(alertRepository).saveAlert(result);
+    }
+
+    @Test
+    void acknowledgeAlertShouldThrowWhenAlertNotFound() {
+        when(alertRepository.findAlerts(null)).thenReturn(Collections.emptyList());
+
+        assertThatThrownBy(() -> alertService.acknowledgeAlert("non-existent"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("System alert not found: non-existent");
+    }
+
+    @Test
+    void clearAcknowledgedShouldReturnDeletedCount() {
+        when(alertRepository.deleteAcknowledgedAlerts()).thenReturn(3);
+
+        int result = alertService.clearAcknowledged();
+
+        assertThat(result).isEqualTo(3);
+        verify(alertRepository).deleteAcknowledgedAlerts();
+    }
+
+    @Test
+    void clearAcknowledgedShouldReturnZeroWhenNoneAcknowledged() {
+        when(alertRepository.deleteAcknowledgedAlerts()).thenReturn(0);
+
+        int result = alertService.clearAcknowledged();
+
+        assertThat(result).isZero();
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/ops/audit/AuditServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/audit/AuditServiceTest.java
new file mode 100644
index 0000000..e044632
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/ops/audit/AuditServiceTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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 com.rocketmq.studio.ops.audit;
+
+import com.rocketmq.studio.common.domain.PageResult;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AuditServiceTest {
+
+    @Mock
+    private AuditRepository auditRepository;
+
+    @InjectMocks
+    private AuditService auditService;
+
+    @Test
+    void queryLogsShouldReturnFirstPage() {
+        AuditRecordVO r1 = AuditRecordVO.builder()
+                .operator("admin").operationType("CREATE").target("topic-a").result("SUCCESS").build();
+        AuditRecordVO r2 = AuditRecordVO.builder()
+                .operator("admin").operationType("DELETE").target("topic-b").result("SUCCESS").build();
+        AuditRecordVO r3 = AuditRecordVO.builder()
+                .operator("user1").operationType("UPDATE").target("topic-c").result("FAILURE").build();
+        when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull()))
+                .thenReturn(Arrays.asList(r1, r2, r3));
+
+        PageResult<AuditRecordVO> result = auditService.queryLogs(1, 2, null, null, null, null, null);
+
+        assertThat(result.getItems()).hasSize(2);
+        assertThat(result.getTotal()).isEqualTo(3);
+        assertThat(result.getPage()).isEqualTo(1);
+        assertThat(result.getSize()).isEqualTo(2);
+        assertThat(result.getItems().get(0).getOperationType()).isEqualTo("CREATE");
+        assertThat(result.getItems().get(1).getOperationType()).isEqualTo("DELETE");
+    }
+
+    @Test
+    void queryLogsShouldReturnSecondPage() {
+        AuditRecordVO r1 = AuditRecordVO.builder().operationType("CREATE").build();
+        AuditRecordVO r2 = AuditRecordVO.builder().operationType("DELETE").build();
+        AuditRecordVO r3 = AuditRecordVO.builder().operationType("UPDATE").build();
+        when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull()))
+                .thenReturn(Arrays.asList(r1, r2, r3));
+
+        PageResult<AuditRecordVO> result = auditService.queryLogs(2, 2, null, null, null, null, null);
+
+        assertThat(result.getItems()).hasSize(1);
+        assertThat(result.getTotal()).isEqualTo(3);
+        assertThat(result.getPage()).isEqualTo(2);
+        assertThat(result.getItems().get(0).getOperationType()).isEqualTo("UPDATE");
+    }
+
+    @Test
+    void queryLogsShouldReturnEmptyPageWhenNoRecords() {
+        when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull()))
+                .thenReturn(Collections.emptyList());
+
+        PageResult<AuditRecordVO> result = auditService.queryLogs(1, 10, null, null, null, null, null);
+
+        assertThat(result.getItems()).isEmpty();
+        assertThat(result.getTotal()).isZero();
+        assertThat(result.getPage()).isEqualTo(1);
+        assertThat(result.getSize()).isEqualTo(10);
+    }
+
+    @Test
+    void queryLogsShouldReturnEmptyWhenPageExceedsTotal() {
+        AuditRecordVO r1 = AuditRecordVO.builder().operationType("CREATE").build();
+        when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull()))
+                .thenReturn(List.of(r1));
+
+        PageResult<AuditRecordVO> result = auditService.queryLogs(5, 10, null, null, null, null, null);
+
+        assertThat(result.getItems()).isEmpty();
+        assertThat(result.getTotal()).isEqualTo(1);
+    }
+
+    @Test
+    void queryLogsShouldPassSearchFilterToRepository() {
+        when(auditRepository.findAll(eq("topic-a"), isNull(), isNull(), isNull(), isNull()))
+                .thenReturn(Collections.emptyList());
+
+        auditService.queryLogs(1, 10, "topic-a", null, null, null, null);
+
+        verify(auditRepository).findAll(eq("topic-a"), isNull(), isNull(), isNull(), isNull());
+    }
+
+    @Test
+    void queryLogsShouldPassOperationTypeFilterToRepository() {
+        when(auditRepository.findAll(isNull(), eq("CREATE"), isNull(), isNull(), isNull()))
+                .thenReturn(Collections.emptyList());
+
+        auditService.queryLogs(1, 10, null, "CREATE", null, null, null);
+
+        verify(auditRepository).findAll(isNull(), eq("CREATE"), isNull(), isNull(), isNull());
+    }
+
+    @Test
+    void queryLogsShouldPassResultFilterToRepository() {
+        when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), eq("SUCCESS")))
+                .thenReturn(Collections.emptyList());
+
+        auditService.queryLogs(1, 10, null, null, null, null, "SUCCESS");
+
+        verify(auditRepository).findAll(isNull(), isNull(), isNull(), isNull(), eq("SUCCESS"));
+    }
+
+    @Test
+    void queryLogsShouldParseDateRange() {
+        when(auditRepository.findAll(isNull(), isNull(), any(LocalDateTime.class), any(LocalDateTime.class), isNull()))
+                .thenReturn(Collections.emptyList());
+
+        auditService.queryLogs(1, 10, null, null, "2025-01-01", "2025-01-31", null);
+
+        ArgumentCaptor<LocalDateTime> startCaptor = ArgumentCaptor.forClass(LocalDateTime.class);
+        ArgumentCaptor<LocalDateTime> endCaptor = ArgumentCaptor.forClass(LocalDateTime.class);
+        verify(auditRepository).findAll(isNull(), isNull(), startCaptor.capture(), endCaptor.capture(), isNull());
+
+        assertThat(startCaptor.getValue()).isEqualTo(LocalDateTime.of(2025, 1, 1, 0, 0, 0));
+        assertThat(endCaptor.getValue().getYear()).isEqualTo(2025);
+        assertThat(endCaptor.getValue().getMonthValue()).isEqualTo(1);
+        assertThat(endCaptor.getValue().getDayOfMonth()).isEqualTo(31);
+        assertThat(endCaptor.getValue().getHour()).isEqualTo(23);
+        assertThat(endCaptor.getValue().getMinute()).isEqualTo(59);
+    }
+
+    @Test
+    void queryLogsShouldHandleInvalidDateFormat() {
+        when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull()))
+                .thenReturn(Collections.emptyList());
+
+        PageResult<AuditRecordVO> result = auditService.queryLogs(1, 10, null, null, "invalid-date", null, null);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getItems()).isEmpty();
+    }
+
+    @Test
+    void cleanupLogsShouldDeleteOldRecords() {
+        when(auditRepository.deleteBefore(any(LocalDateTime.class))).thenReturn(42);
+
+        int result = auditService.cleanupLogs(30);
+
+        assertThat(result).isEqualTo(42);
+        ArgumentCaptor<LocalDateTime> captor = ArgumentCaptor.forClass(LocalDateTime.class);
+        verify(auditRepository).deleteBefore(captor.capture());
+
+        LocalDateTime cutoff = captor.getValue();
+        LocalDateTime expected = LocalDateTime.now().minusDays(30);
+        assertThat(cutoff).isCloseTo(expected, org.assertj.core.api.Assertions.within(2, java.time.temporal.ChronoUnit.SECONDS));
+    }
+
+    @Test
+    void cleanupLogsShouldReturnZeroWhenNoOldRecords() {
+        when(auditRepository.deleteBefore(any(LocalDateTime.class))).thenReturn(0);
+
+        int result = auditService.cleanupLogs(90);
+
+        assertThat(result).isZero();
+    }
+
+    @Test
+    void queryLogsShouldHandleAllFiltersTogether() {
+        when(auditRepository.findAll(eq("admin"), eq("DELETE"), any(LocalDateTime.class),
+                any(LocalDateTime.class), eq("FAILURE")))
+                .thenReturn(Collections.emptyList());
+
+        auditService.queryLogs(1, 10, "admin", "DELETE", "2025-06-01", "2025-06-30", "FAILURE");
+
+        verify(auditRepository).findAll(eq("admin"), eq("DELETE"), any(LocalDateTime.class),
+                any(LocalDateTime.class), eq("FAILURE"));
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardControllerTest.java b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardControllerTest.java
new file mode 100644
index 0000000..41fa848
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardControllerTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.rocketmq.studio.ops.dashboard;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(DashboardController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class DashboardControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private DashboardService dashboardService;
+
+    @Test
+    void getDashboardShouldReturnStatsAndClusters() throws Exception {
+        DashboardStatsVO stats = DashboardStatsVO.builder()
+                .totalClusters(2)
+                .healthyClusters(2)
+                .totalBrokers(6)
+                .totalProxies(2)
+                .totalNameServers(4)
+                .totalTopics(80)
+                .totalConsumerGroups(30)
+                .totalMessagesToday(500_000L)
+                .messagesPerSecond(250L)
+                .tpsIn(150L)
+                .tpsOut(100L)
+                .build();
+
+        ClusterOverviewVO cluster = ClusterOverviewVO.builder()
+                .id("c1")
+                .name("prod-cluster")
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .status(ClusterStatus.healthy)
+                .brokers(3)
+                .proxies(1)
+                .topics(40)
+                .groups(15)
+                .tpsIn(100)
+                .tpsOut(80)
+                .version("5.1.0")
+                .throughput(List.of(5, 10, 15))
+                .build();
+
+        DashboardDataVO data = DashboardDataVO.builder()
+                .stats(stats)
+                .clusters(List.of(cluster))
+                .build();
+
+        when(dashboardService.getDashboard()).thenReturn(data);
+
+        mockMvc.perform(get("/api/dashboard"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"))
+                .andExpect(jsonPath("$.data.stats.totalClusters").value(2))
+                .andExpect(jsonPath("$.data.stats.healthyClusters").value(2))
+                .andExpect(jsonPath("$.data.stats.totalBrokers").value(6))
+                .andExpect(jsonPath("$.data.stats.totalTopics").value(80))
+                .andExpect(jsonPath("$.data.stats.messagesPerSecond").value(250))
+                .andExpect(jsonPath("$.data.clusters").isArray())
+                .andExpect(jsonPath("$.data.clusters[0].id").value("c1"))
+                .andExpect(jsonPath("$.data.clusters[0].name").value("prod-cluster"))
+                .andExpect(jsonPath("$.data.clusters[0].status").value("healthy"))
+                .andExpect(jsonPath("$.data.clusters[0].version").value("5.1.0"));
+
+        verify(dashboardService).getDashboard();
+    }
+
+    @Test
+    void getDashboardShouldReturnEmptyClustersWhenNoneExist() throws Exception {
+        DashboardDataVO data = DashboardDataVO.builder()
+                .stats(DashboardStatsVO.builder().totalClusters(0).build())
+                .clusters(List.of())
+                .build();
+
+        when(dashboardService.getDashboard()).thenReturn(data);
+
+        mockMvc.perform(get("/api/dashboard"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.stats.totalClusters").value(0))
+                .andExpect(jsonPath("$.data.clusters").isArray())
+                .andExpect(jsonPath("$.data.clusters").isEmpty());
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardServiceTest.java
new file mode 100644
index 0000000..33dbe48
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardServiceTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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 com.rocketmq.studio.ops.dashboard;
+
+import com.rocketmq.studio.common.domain.enums.ClusterStatus;
+import com.rocketmq.studio.common.domain.enums.ClusterType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DashboardServiceTest {
+
+    @Mock
+    private DashboardProvider dashboardProvider;
+
+    @InjectMocks
+    private DashboardService dashboardService;
+
+    @Test
+    void getDashboardShouldReturnStatsAndClusters() {
+        DashboardStatsVO stats = DashboardStatsVO.builder()
+                .totalClusters(3)
+                .healthyClusters(2)
+                .totalBrokers(9)
+                .totalProxies(3)
+                .totalNameServers(6)
+                .totalTopics(120)
+                .totalConsumerGroups(45)
+                .totalMessagesToday(1_000_000L)
+                .messagesPerSecond(500L)
+                .tpsIn(300L)
+                .tpsOut(200L)
+                .build();
+
+        ClusterOverviewVO cluster = ClusterOverviewVO.builder()
+                .id("cluster-1")
+                .name("production")
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .status(ClusterStatus.healthy)
+                .brokers(3)
+                .proxies(1)
+                .topics(50)
+                .groups(20)
+                .tpsIn(100)
+                .tpsOut(80)
+                .version("5.1.0")
+                .throughput(List.of(10, 20, 30))
+                .build();
+
+        DashboardDataVO mockData = DashboardDataVO.builder()
+                .stats(stats)
+                .clusters(List.of(cluster))
+                .build();
+
+        when(dashboardProvider.getDashboardData()).thenReturn(mockData);
+
+        DashboardDataVO result = dashboardService.getDashboard();
+
+        assertThat(result).isNotNull();
+        assertThat(result.getStats()).isNotNull();
+        assertThat(result.getStats().getTotalClusters()).isEqualTo(3);
+        assertThat(result.getStats().getHealthyClusters()).isEqualTo(2);
+        assertThat(result.getStats().getTotalBrokers()).isEqualTo(9);
+        assertThat(result.getStats().getTotalTopics()).isEqualTo(120);
+        assertThat(result.getStats().getMessagesPerSecond()).isEqualTo(500L);
+
+        assertThat(result.getClusters()).hasSize(1);
+        assertThat(result.getClusters().get(0).getName()).isEqualTo("production");
+        assertThat(result.getClusters().get(0).getStatus()).isEqualTo(ClusterStatus.healthy);
+
+        verify(dashboardProvider).getDashboardData();
+    }
+
+    @Test
+    void getDashboardShouldDelegateToProvider() {
+        DashboardDataVO emptyData = DashboardDataVO.builder()
+                .stats(DashboardStatsVO.builder().build())
+                .clusters(List.of())
+                .build();
+
+        when(dashboardProvider.getDashboardData()).thenReturn(emptyData);
+
+        DashboardDataVO result = dashboardService.getDashboard();
+
+        assertThat(result).isNotNull();
+        assertThat(result.getClusters()).isEmpty();
+        verify(dashboardProvider).getDashboardData();
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java b/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java
new file mode 100644
index 0000000..42a3c0e
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(SettingsController.class)
+class SettingsControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @MockBean
+    private SettingsService settingsService;
+
+    @Test
+    void getGeneralSettingsShouldReturnSettings() throws Exception {
+        GeneralSettingsVO settings = GeneralSettingsVO.builder()
+                .theme("dark")
+                .compact(true)
+                .desktopNotify(true)
+                .notifySound(false)
+                .sessionTimeout(30)
+                .requireLogin(true)
+                .llmProvider("openai")
+                .apiKey("sk-xxx")
+                .model("gpt-4")
+                .baseUrl("https://api.openai.com")
+                .build();
+        when(settingsService.getGeneralSettings()).thenReturn(settings);
+
+        mockMvc.perform(get("/api/settings/general"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)))
+                .andExpect(jsonPath("$.message", is("success")))
+                .andExpect(jsonPath("$.data.theme", is("dark")))
+                .andExpect(jsonPath("$.data.compact", is(true)))
+                .andExpect(jsonPath("$.data.desktopNotify", is(true)))
+                .andExpect(jsonPath("$.data.notifySound", is(false)))
+                .andExpect(jsonPath("$.data.sessionTimeout", is(30)))
+                .andExpect(jsonPath("$.data.requireLogin", is(true)))
+                .andExpect(jsonPath("$.data.llmProvider", is("openai")))
+                .andExpect(jsonPath("$.data.model", is("gpt-4")));
+    }
+
+    @Test
+    void saveGeneralSettingsShouldReturnSuccess() throws Exception {
+        GeneralSettingsVO settings = GeneralSettingsVO.builder()
+                .theme("light")
+                .compact(false)
+                .sessionTimeout(60)
+                .build();
+        doNothing().when(settingsService).saveGeneralSettings(any(GeneralSettingsVO.class));
+
+        mockMvc.perform(post("/api/settings/general/save")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(settings)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)))
+                .andExpect(jsonPath("$.data").doesNotExist());
+
+        verify(settingsService).saveGeneralSettings(any(GeneralSettingsVO.class));
+    }
+
+    @Test
+    void listDataSourcesShouldReturnAllSources() throws Exception {
+        DataSourceVO ds1 = DataSourceVO.builder().key("ds-1").name("Production").type("rocketmq")
+                .url("prod:9876").status("connected").build();
+        DataSourceVO ds2 = DataSourceVO.builder().key("ds-2").name("Staging").type("rocketmq")
+                .url("staging:9876").status("disconnected").build();
+        when(settingsService.listDataSources()).thenReturn(Arrays.asList(ds1, ds2));
+
+        mockMvc.perform(get("/api/settings/datasources"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)))
+                .andExpect(jsonPath("$.data", hasSize(2)))
+                .andExpect(jsonPath("$.data[0].key", is("ds-1")))
+                .andExpect(jsonPath("$.data[0].name", is("Production")))
+                .andExpect(jsonPath("$.data[0].status", is("connected")))
+                .andExpect(jsonPath("$.data[1].key", is("ds-2")))
+                .andExpect(jsonPath("$.data[1].name", is("Staging")));
+    }
+
+    @Test
+    void listDataSourcesShouldReturnEmptyList() throws Exception {
+        when(settingsService.listDataSources()).thenReturn(Collections.emptyList());
+
+        mockMvc.perform(get("/api/settings/datasources"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)))
+                .andExpect(jsonPath("$.data", hasSize(0)));
+    }
+
+    @Test
+    void createDataSourceShouldReturnCreatedSource() throws Exception {
+        DataSourceVO input = DataSourceVO.builder().name("New DS").type("rocketmq")
+                .url("new-host:9876").build();
+        DataSourceVO created = DataSourceVO.builder().key("ds-new").name("New DS").type("rocketmq")
+                .url("new-host:9876").status("connected").build();
+        when(settingsService.createDataSource(any(DataSourceVO.class))).thenReturn(created);
+
+        mockMvc.perform(post("/api/settings/datasources/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(input)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)))
+                .andExpect(jsonPath("$.data.key", is("ds-new")))
+                .andExpect(jsonPath("$.data.name", is("New DS")))
+                .andExpect(jsonPath("$.data.status", is("connected")));
+    }
+
+    @Test
+    void updateDataSourceShouldReturnUpdatedSource() throws Exception {
+        DataSourceVO input = DataSourceVO.builder().key("ds-1").name("Updated DS").type("rocketmq")
+                .url("updated:9876").build();
+        when(settingsService.updateDataSource(any(DataSourceVO.class))).thenReturn(input);
+
+        mockMvc.perform(post("/api/settings/datasources/update")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(input)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)))
+                .andExpect(jsonPath("$.data.key", is("ds-1")))
+                .andExpect(jsonPath("$.data.name", is("Updated DS")));
+    }
+
+    @Test
+    void deleteDataSourceShouldReturnSuccess() throws Exception {
+        doNothing().when(settingsService).deleteDataSource("ds-1");
+
+        mockMvc.perform(post("/api/settings/datasources/delete")
+                        .param("key", "ds-1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)));
+
+        verify(settingsService).deleteDataSource("ds-1");
+    }
+
+    @Test
+    void testDataSourceShouldReturnTestResult() throws Exception {
+        DataSourceTestDTO request = DataSourceTestDTO.builder()
+                .url("localhost:9876")
+                .type("rocketmq")
+                .build();
+        DataSourceTestResultVO testResult = DataSourceTestResultVO.builder()
+                .success(true)
+                .message("Connection successful")
+                .build();
+        when(settingsService.testDataSource(any(DataSourceTestDTO.class))).thenReturn(testResult);
+
+        mockMvc.perform(post("/api/settings/datasources/test")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(request)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", is(200)))
+                .andExpect(jsonPath("$.data.success", is(true)))
+                .andExpect(jsonPath("$.data.message", is("Connection successful")));
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java b/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java
new file mode 100644
index 0000000..331197e
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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 com.rocketmq.studio.settings;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class SettingsServiceTest {
+
+    @Mock
+    private SettingsRepository settingsRepository;
+
+    @InjectMocks
+    private SettingsService settingsService;
+
+    @Test
+    void getGeneralSettingsShouldReturnCurrentSettings() {
+        GeneralSettingsVO settings = GeneralSettingsVO.builder()
+                .theme("dark")
+                .compact(true)
+                .desktopNotify(true)
+                .notifySound(false)
+                .sessionTimeout(30)
+                .requireLogin(true)
+                .llmProvider("openai")
+                .model("gpt-4")
+                .build();
+        when(settingsRepository.loadGeneralSettings()).thenReturn(settings);
+
+        GeneralSettingsVO result = settingsService.getGeneralSettings();
+
+        assertThat(result.getTheme()).isEqualTo("dark");
+        assertThat(result.isCompact()).isTrue();
+        assertThat(result.isDesktopNotify()).isTrue();
+        assertThat(result.isNotifySound()).isFalse();
+        assertThat(result.getSessionTimeout()).isEqualTo(30);
+        assertThat(result.isRequireLogin()).isTrue();
+        assertThat(result.getLlmProvider()).isEqualTo("openai");
+        assertThat(result.getModel()).isEqualTo("gpt-4");
+    }
+
+    @Test
+    void saveGeneralSettingsShouldDelegateToRepository() {
+        GeneralSettingsVO settings = GeneralSettingsVO.builder()
+                .theme("light")
+                .compact(false)
+                .sessionTimeout(60)
+                .build();
+        doNothing().when(settingsRepository).saveGeneralSettings(settings);
+
+        settingsService.saveGeneralSettings(settings);
+
+        verify(settingsRepository).saveGeneralSettings(settings);
+    }
+
+    @Test
+    void listDataSourcesShouldReturnAllSources() {
+        DataSourceVO ds1 = DataSourceVO.builder().key("ds-1").name("Production").type("rocketmq")
+                .url("localhost:9876").status("connected").build();
+        DataSourceVO ds2 = DataSourceVO.builder().key("ds-2").name("Staging").type("rocketmq")
+                .url("staging:9876").status("disconnected").build();
+        when(settingsRepository.findAllDataSources()).thenReturn(Arrays.asList(ds1, ds2));
+
+        List<DataSourceVO> result = settingsService.listDataSources();
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getName()).isEqualTo("Production");
+        assertThat(result.get(0).getStatus()).isEqualTo("connected");
+        assertThat(result.get(1).getName()).isEqualTo("Staging");
+        assertThat(result.get(1).getStatus()).isEqualTo("disconnected");
+    }
+
+    @Test
+    void listDataSourcesShouldReturnEmptyListWhenNoSources() {
+        when(settingsRepository.findAllDataSources()).thenReturn(Collections.emptyList());
+
+        List<DataSourceVO> result = settingsService.listDataSources();
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void createDataSourceShouldDelegateToRepository() {
+        DataSourceVO input = DataSourceVO.builder().name("New DS").type("rocketmq")
+                .url("new-host:9876").build();
+        DataSourceVO saved = DataSourceVO.builder().key("ds-new").name("New DS").type("rocketmq")
+                .url("new-host:9876").status("connected").build();
+        when(settingsRepository.saveDataSource(any(DataSourceVO.class))).thenReturn(saved);
+
+        DataSourceVO result = settingsService.createDataSource(input);
+
+        assertThat(result.getKey()).isEqualTo("ds-new");
+        assertThat(result.getName()).isEqualTo("New DS");
+        assertThat(result.getStatus()).isEqualTo("connected");
+        verify(settingsRepository).saveDataSource(input);
+    }
+
+    @Test
+    void updateDataSourceShouldDelegateToRepository() {
+        DataSourceVO input = DataSourceVO.builder().key("ds-1").name("Updated DS").type("rocketmq")
+                .url("updated-host:9876").build();
+        when(settingsRepository.saveDataSource(any(DataSourceVO.class))).thenReturn(input);
+
+        DataSourceVO result = settingsService.updateDataSource(input);
+
+        assertThat(result.getKey()).isEqualTo("ds-1");
+        assertThat(result.getName()).isEqualTo("Updated DS");
+        verify(settingsRepository).saveDataSource(input);
+    }
+
+    @Test
+    void deleteDataSourceShouldDelegateToRepository() {
+        doNothing().when(settingsRepository).deleteDataSource("ds-1");
+
+        settingsService.deleteDataSource("ds-1");
+
+        verify(settingsRepository).deleteDataSource("ds-1");
+    }
+
+    @Test
+    void testConnectionShouldReturnSuccess() {
+        DataSourceTestDTO request = DataSourceTestDTO.builder()
+                .url("localhost:9876")
+                .type("rocketmq")
+                .build();
+
+        DataSourceTestResultVO result = settingsService.testDataSource(request);
+
+        assertThat(result.isSuccess()).isTrue();
+        assertThat(result.getMessage()).isEqualTo("Connection successful");
+    }
+
+    @Test
+    void testConnectionShouldReturnSuccessForAnyInput() {
+        DataSourceTestDTO request = DataSourceTestDTO.builder()
+                .url("invalid-host:9999")
+                .type("unknown")
+                .auth("bad-auth")
+                .build();
+
+        DataSourceTestResultVO result = settingsService.testDataSource(request);
+
+        assertThat(result.isSuccess()).isTrue();
+        assertThat(result.getMessage()).isEqualTo("Connection successful");
+    }
+}


(rocketmq-dashboard) 01/09: chore: initialize project with license and git config

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit 57605f0b6303b8cc910df732513cc632f646ac9a
Author: lizhimins <70...@qq.com>
AuthorDate: Wed Jul 1 09:47:00 2026 +0800

    chore: initialize project with license and git config
    
    - Add Apache 2.0 LICENSE and NOTICE
    - Configure .gitignore for IDE, build artifacts, dependencies
    - Add husky pre-commit hook
---
 .gitignore        |  24 +++++++
 .husky/pre-commit |   1 +
 LICENSE           | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 NOTICE            |   5 ++
 4 files changed, 231 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..198b806
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# IDE
+.idea/
+.vscode/
+*.iml
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Build artifacts
+dist/
+target/
+
+# Dependencies
+node_modules/
+
+# Generated docs
+docs/*.html
+
+# Husky internal files (auto-generated scaffolds)
+.husky/_/
+
+# Deploy environment (local only)
+deploy/.env
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..466cbf4
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+cd web && npx lint-staged
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c8aa782
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to the Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by the Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding any notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. Please also get an in-depth
+      understanding of the Apache License by reading the FAQ at
+      http://www.apache.org/foundation/license-faq.html
+
+   Copyright 2026 RocketMQ Studio Contributors
+
+   Licensed 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.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..fde8f8e
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+RocketMQ Studio
+Copyright 2026 RocketMQ Studio Contributors
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).


(rocketmq-dashboard) 07/09: feat: implement backend instance management module

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit cfe953b9c29c62801a2d789b0da87ee83624d7c0
Author: lizhimins <70...@qq.com>
AuthorDate: Tue Jul 7 13:41:00 2026 +0800

    feat: implement backend instance management module
    
    - Instance CRUD (InstanceController, InstanceService)
    - Topic management with metadata (TopicController, MetadataService)
    - Consumer group monitoring (ConsumerGroupController)
    - Message query and trace (MessageController)
    - Dead letter queue management (DLQController)
    - ACL rule management (AclController)
    - In-memory repository implementations
    - Unit tests for all services and controllers
---
 .../instance/InMemoryInstanceRepository.java       | 115 +++++++++
 .../studio/instance/InstanceController.java        |  62 +++++
 .../studio/instance/InstanceRepository.java        |  39 +++
 .../rocketmq/studio/instance/InstanceService.java  | 104 ++++++++
 .../com/rocketmq/studio/instance/InstanceVO.java   |  40 +++
 .../studio/instance/acl/AclController.java         |  71 ++++++
 .../studio/instance/acl/AclRepository.java         |  34 +++
 .../rocketmq/studio/instance/acl/AclRuleVO.java    |  42 ++++
 .../rocketmq/studio/instance/acl/AclService.java   |  75 ++++++
 .../rocketmq/studio/instance/acl/AclUserVO.java    |  39 +++
 .../studio/instance/acl/InMemoryAclRepository.java |  73 ++++++
 .../studio/instance/dlq/DLQController.java         |  52 ++++
 .../rocketmq/studio/instance/dlq/DLQGroupVO.java   |  37 +++
 .../rocketmq/studio/instance/dlq/DLQProvider.java  |  25 ++
 .../studio/instance/dlq/DLQProviderStub.java       |  39 +++
 .../rocketmq/studio/instance/dlq/DLQService.java   |  41 ++++
 .../instance/group/ConsumerGroupController.java    |  82 +++++++
 .../studio/instance/group/ConsumerGroupVO.java     |  43 ++++
 .../studio/instance/group/ConsumerInstanceVO.java  |  40 +++
 .../studio/instance/group/QueueProgressVO.java     |  34 +++
 .../studio/instance/group/SubscriptionEntryVO.java |  34 +++
 .../studio/instance/message/ConsumerStatusVO.java  |  34 +++
 .../studio/instance/message/MessageController.java |  50 ++++
 .../studio/instance/message/MessageProvider.java   |  26 ++
 .../instance/message/MessageProviderStub.java      |  43 ++++
 .../studio/instance/message/MessageRecordVO.java   |  41 ++++
 .../studio/instance/message/MessageService.java    |  41 ++++
 .../studio/instance/message/TraceNodeVO.java       |  34 +++
 .../studio/instance/message/TraceRecordVO.java     |  33 +++
 .../studio/instance/topic/AdminClient.java         |  32 +++
 .../studio/instance/topic/BrokerRouteVO.java       |  35 +++
 .../instance/topic/CloudMetadataProvider.java      |  61 +++++
 .../studio/instance/topic/LiteTopicVO.java         |  26 ++
 .../studio/instance/topic/MetadataProvider.java    |  32 +++
 .../studio/instance/topic/MetadataService.java     | 117 +++++++++
 .../studio/instance/topic/NameSrvAdminClient.java  |  72 ++++++
 .../studio/instance/topic/NamespaceController.java |  39 +++
 .../studio/instance/topic/NamespaceVO.java         |  29 +++
 .../studio/instance/topic/ProxyAdminClient.java    |  25 ++
 .../studio/instance/topic/SendMessageDTO.java      |  36 +++
 .../studio/instance/topic/SendMessageVO.java       |  32 +++
 .../studio/instance/topic/TopicConsumerVO.java     |  35 +++
 .../studio/instance/topic/TopicController.java     |  77 ++++++
 .../rocketmq/studio/instance/topic/TopicVO.java    |  39 +++
 .../studio/instance/InstanceControllerTest.java    | 191 +++++++++++++++
 .../studio/instance/InstanceServiceTest.java       | 271 +++++++++++++++++++++
 .../studio/instance/acl/AclControllerTest.java     | 140 +++++++++++
 .../studio/instance/acl/AclServiceTest.java        | 136 +++++++++++
 .../studio/instance/dlq/DLQControllerTest.java     | 125 ++++++++++
 .../studio/instance/dlq/DLQServiceTest.java        |  90 +++++++
 .../studio/instance/topic/MetadataServiceTest.java | 144 +++++++++++
 .../studio/instance/topic/TopicControllerTest.java | 133 ++++++++++
 52 files changed, 3340 insertions(+)

diff --git a/server/src/main/java/com/rocketmq/studio/instance/InMemoryInstanceRepository.java b/server/src/main/java/com/rocketmq/studio/instance/InMemoryInstanceRepository.java
new file mode 100644
index 0000000..36d1dd6
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/InMemoryInstanceRepository.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.rocketmq.studio.instance;
+
+import com.rocketmq.studio.common.domain.enums.InstanceType;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+@Component
+public class InMemoryInstanceRepository implements InstanceRepository {
+
+    private final Map<String, InstanceVO> store = new ConcurrentHashMap<>();
+
+    public InMemoryInstanceRepository() {
+        initData();
+    }
+
+    private void initData() {
+        addInstance("inst-1", "production-proxy", "Production Proxy InstanceVO",
+                InstanceType.PROXY, "10.0.1.100:8080", 42, 18,
+                LocalDateTime.now().minusDays(90));
+        addInstance("inst-2", "staging-proxy", "Staging Proxy InstanceVO",
+                InstanceType.PROXY, "10.0.2.100:8080", 15, 8,
+                LocalDateTime.now().minusDays(60));
+        addInstance("inst-3", "dev-direct", "Development Direct InstanceVO",
+                InstanceType.DIRECT, "10.0.3.100:10911", 8, 3,
+                LocalDateTime.now().minusDays(30));
+    }
+
+    private void addInstance(String id, String name, String remark, InstanceType type,
+                             String endpoint, int topicCount, int consumerGroupCount,
+                             LocalDateTime createdAt) {
+        InstanceVO instance = InstanceVO.builder()
+                .name(name)
+                .remark(remark)
+                .type(type)
+                .endpoint(endpoint)
+                .topicCount(topicCount)
+                .consumerGroupCount(consumerGroupCount)
+                .build();
+        instance.setId(id);
+        instance.setCreatedAt(createdAt);
+        instance.setUpdatedAt(createdAt);
+        store.put(id, instance);
+    }
+
+    @Override
+    public List<InstanceVO> findAll() {
+        return new ArrayList<>(store.values());
+    }
+
+    @Override
+    public List<InstanceVO> findByType(InstanceType type) {
+        return store.values().stream()
+                .filter(i -> i.getType() == type)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<InstanceVO> search(String keyword) {
+        String lower = keyword.toLowerCase();
+        return store.values().stream()
+                .filter(i -> i.getName().toLowerCase().contains(lower)
+                        || i.getRemark() != null && i.getRemark().toLowerCase().contains(lower))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<InstanceVO> findByTypeAndSearch(InstanceType type, String keyword) {
+        String lower = keyword.toLowerCase();
+        return store.values().stream()
+                .filter(i -> i.getType() == type)
+                .filter(i -> i.getName().toLowerCase().contains(lower)
+                        || i.getRemark() != null && i.getRemark().toLowerCase().contains(lower))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Optional<InstanceVO> findById(String id) {
+        return Optional.ofNullable(store.get(id));
+    }
+
+    @Override
+    public InstanceVO save(InstanceVO instance) {
+        store.put(instance.getId(), instance);
+        return instance;
+    }
+
+    @Override
+    public void deleteById(String id) {
+        store.remove(id);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/InstanceController.java b/server/src/main/java/com/rocketmq/studio/instance/InstanceController.java
new file mode 100644
index 0000000..a7d1186
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/InstanceController.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.instance;
+
+import com.rocketmq.studio.common.domain.Result;
+import com.rocketmq.studio.common.domain.enums.InstanceType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/instances")
+@RequiredArgsConstructor
+public class InstanceController {
+
+    private final InstanceService instanceService;
+
+    @GetMapping
+    public Result<List<InstanceVO>> listInstances(
+            @RequestParam(required = false) InstanceType type,
+            @RequestParam(required = false) String search) {
+        return Result.ok(instanceService.listInstances(type, search));
+    }
+
+    @PostMapping("/create")
+    public Result<InstanceVO> createInstance(@RequestBody InstanceVO instance) {
+        return Result.ok(instanceService.createInstance(instance));
+    }
+
+    @PostMapping("/update")
+    public Result<InstanceVO> updateInstance(@RequestBody InstanceVO instance) {
+        return Result.ok(instanceService.updateInstance(instance));
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> deleteInstance(@RequestBody Map<String, String> body) {
+        instanceService.deleteInstance(body.get("id"));
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/InstanceRepository.java b/server/src/main/java/com/rocketmq/studio/instance/InstanceRepository.java
new file mode 100644
index 0000000..2361465
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/InstanceRepository.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.rocketmq.studio.instance;
+
+import com.rocketmq.studio.common.domain.enums.InstanceType;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface InstanceRepository {
+    List<InstanceVO> findAll();
+
+    List<InstanceVO> findByType(InstanceType type);
+
+    List<InstanceVO> search(String keyword);
+
+    List<InstanceVO> findByTypeAndSearch(InstanceType type, String keyword);
+
+    Optional<InstanceVO> findById(String id);
+
+    InstanceVO save(InstanceVO instance);
+
+    void deleteById(String id);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/InstanceService.java b/server/src/main/java/com/rocketmq/studio/instance/InstanceService.java
new file mode 100644
index 0000000..628d190
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/InstanceService.java
@@ -0,0 +1,104 @@
+/*
+ * 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 com.rocketmq.studio.instance;
+
+import com.rocketmq.studio.common.domain.enums.InstanceType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class InstanceService {
+
+    private final InstanceRepository instanceRepository;
+
+    public List<InstanceVO> listInstances(InstanceType type, String search) {
+        log.debug("Listing instances, type={}, search={}", type, search);
+
+        if (type != null && search != null && !search.isBlank()) {
+            return instanceRepository.findByTypeAndSearch(type, search);
+        } else if (type != null) {
+            return instanceRepository.findByType(type);
+        } else if (search != null && !search.isBlank()) {
+            return instanceRepository.search(search);
+        }
+        return instanceRepository.findAll();
+    }
+
+    public InstanceVO createInstance(InstanceVO instance) {
+        log.info("Creating instance: {}", instance.getName());
+
+        if (instance.getName() == null || instance.getName().isBlank()) {
+            throw new BusinessException(400, "InstanceVO name is required");
+        }
+        if (instance.getEndpoint() == null || instance.getEndpoint().isBlank()) {
+            throw new BusinessException(400, "InstanceVO endpoint is required");
+        }
+
+        instance.setId(UUID.randomUUID().toString());
+        instance.setCreatedAt(LocalDateTime.now());
+        instance.setUpdatedAt(LocalDateTime.now());
+        return instanceRepository.save(instance);
+    }
+
+    public InstanceVO updateInstance(InstanceVO instance) {
+        log.info("Updating instance: {}", instance.getId());
+
+        if (instance.getId() == null || instance.getId().isBlank()) {
+            throw new BusinessException(400, "InstanceVO ID is required");
+        }
+
+        InstanceVO existing = instanceRepository.findById(instance.getId())
+                .orElseThrow(() -> new BusinessException(404, "InstanceVO not found: " + instance.getId()));
+
+        if (instance.getName() != null) {
+            existing.setName(instance.getName());
+        }
+        if (instance.getType() != null) {
+            existing.setType(instance.getType());
+        }
+        if (instance.getEndpoint() != null) {
+            existing.setEndpoint(instance.getEndpoint());
+        }
+        if (instance.getRemark() != null) {
+            existing.setRemark(instance.getRemark());
+        }
+        existing.setUpdatedAt(LocalDateTime.now());
+
+        return instanceRepository.save(existing);
+    }
+
+    public void deleteInstance(String id) {
+        log.info("Deleting instance: {}", id);
+
+        if (id == null || id.isBlank()) {
+            throw new BusinessException(400, "InstanceVO ID is required");
+        }
+
+        instanceRepository.findById(id)
+                .orElseThrow(() -> new BusinessException(404, "InstanceVO not found: " + id));
+        instanceRepository.deleteById(id);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/InstanceVO.java b/server/src/main/java/com/rocketmq/studio/instance/InstanceVO.java
new file mode 100644
index 0000000..dd4f1c9
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/InstanceVO.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.rocketmq.studio.instance;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import com.rocketmq.studio.common.domain.enums.InstanceType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class InstanceVO extends BaseEntity {
+    private String name;
+    private String remark;
+    private InstanceType type;
+    private String endpoint;
+    private int topicCount;
+    private int consumerGroupCount;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/acl/AclController.java b/server/src/main/java/com/rocketmq/studio/instance/acl/AclController.java
new file mode 100644
index 0000000..3d1d83c
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/AclController.java
@@ -0,0 +1,71 @@
+/*
+ * 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 com.rocketmq.studio.instance.acl;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/acl")
+@RequiredArgsConstructor
+public class AclController {
+
+    private final AclService aclService;
+
+    @GetMapping("/rules")
+    public Result<List<AclRuleVO>> listRules(
+            @RequestParam(required = false) String clusterId,
+            @RequestParam(required = false) String principal) {
+        return Result.ok(aclService.listRules(clusterId, principal));
+    }
+
+    @PostMapping("/rules/create")
+    public Result<AclRuleVO> createRule(@RequestBody AclRuleVO rule) {
+        return Result.ok(aclService.createRule(rule));
+    }
+
+    @PostMapping("/rules/delete")
+    public Result<Void> deleteRule(@RequestBody Map<String, String> request) {
+        aclService.deleteRule(request.get("id"));
+        return Result.ok();
+    }
+
+    @GetMapping("/users")
+    public Result<List<AclUserVO>> listUsers() {
+        return Result.ok(aclService.listUsers());
+    }
+
+    @PostMapping("/users/create")
+    public Result<AclUserVO> createUser(@RequestBody AclUserVO user) {
+        return Result.ok(aclService.createUser(user));
+    }
+
+    @PostMapping("/users/delete")
+    public Result<Void> deleteUser(@RequestBody Map<String, String> request) {
+        aclService.deleteUser(request.get("id"));
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/acl/AclRepository.java b/server/src/main/java/com/rocketmq/studio/instance/acl/AclRepository.java
new file mode 100644
index 0000000..fab77cc
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/AclRepository.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.rocketmq.studio.instance.acl;
+
+
+import java.util.List;
+
+public interface AclRepository {
+    List<AclRuleVO> findRules(String clusterId, String principal);
+
+    AclRuleVO saveRule(AclRuleVO rule);
+
+    void deleteRule(String id);
+
+    List<AclUserVO> findUsers();
+
+    AclUserVO saveUser(AclUserVO user);
+
+    void deleteUser(String id);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/acl/AclRuleVO.java b/server/src/main/java/com/rocketmq/studio/instance/acl/AclRuleVO.java
new file mode 100644
index 0000000..2e36e17
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/AclRuleVO.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.rocketmq.studio.instance.acl;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AclRuleVO {
+    private String id;
+    private String principal;
+    private String resource;
+    private String resourceType;
+    private String resourcePattern;
+    private List<String> actions;
+    private String decision;
+    private String scope;
+    private Integer aclVersion;
+    private LocalDateTime createdAt;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/acl/AclService.java b/server/src/main/java/com/rocketmq/studio/instance/acl/AclService.java
new file mode 100644
index 0000000..537b81e
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/AclService.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.acl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AclService {
+
+    private final AclRepository aclRepository;
+
+
+    public List<AclRuleVO> listRules(String clusterId, String principal) {
+        log.info("Listing ACL rules for clusterId={}, principal={}", clusterId, principal);
+        return aclRepository.findRules(clusterId, principal);
+    }
+
+
+    public AclRuleVO createRule(AclRuleVO rule) {
+        log.info("Creating ACL rule for principal={}", rule.getPrincipal());
+        rule.setId(UUID.randomUUID().toString());
+        rule.setCreatedAt(LocalDateTime.now());
+        return aclRepository.saveRule(rule);
+    }
+
+
+    public void deleteRule(String id) {
+        log.info("Deleting ACL rule id={}", id);
+        aclRepository.deleteRule(id);
+    }
+
+
+    public List<AclUserVO> listUsers() {
+        log.info("Listing ACL users");
+        return aclRepository.findUsers();
+    }
+
+
+    public AclUserVO createUser(AclUserVO user) {
+        log.info("Creating ACL user username={}", user.getUsername());
+        user.setId(UUID.randomUUID().toString());
+        user.setAccessKey(UUID.randomUUID().toString().replace("-", ""));
+        user.setSecretKey(UUID.randomUUID().toString().replace("-", ""));
+        user.setCreatedAt(LocalDateTime.now());
+        return aclRepository.saveUser(user);
+    }
+
+
+    public void deleteUser(String id) {
+        log.info("Deleting ACL user id={}", id);
+        aclRepository.deleteUser(id);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/acl/AclUserVO.java b/server/src/main/java/com/rocketmq/studio/instance/acl/AclUserVO.java
new file mode 100644
index 0000000..5fbea0b
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/AclUserVO.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.rocketmq.studio.instance.acl;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AclUserVO {
+    private String id;
+    private String username;
+    private String accessKey;
+    private String secretKey;
+    private boolean admin;
+    private List<String> clusters;
+    private LocalDateTime createdAt;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/acl/InMemoryAclRepository.java b/server/src/main/java/com/rocketmq/studio/instance/acl/InMemoryAclRepository.java
new file mode 100644
index 0000000..909b775
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/InMemoryAclRepository.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.acl;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Component
+public class InMemoryAclRepository implements AclRepository {
+
+    private final Map<String, AclRuleVO> rules = new ConcurrentHashMap<>();
+    private final Map<String, AclUserVO> users = new ConcurrentHashMap<>();
+
+    @Override
+    public List<AclRuleVO> findRules(String clusterId, String principal) {
+        return rules.values().stream()
+                .filter(r -> clusterId == null || clusterId.equals(r.getScope()))
+                .filter(r -> principal == null || principal.equals(r.getPrincipal()))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public AclRuleVO saveRule(AclRuleVO rule) {
+        rules.put(rule.getId(), rule);
+        log.debug("Saved ACL rule id={}", rule.getId());
+        return rule;
+    }
+
+    @Override
+    public void deleteRule(String id) {
+        rules.remove(id);
+        log.debug("Deleted ACL rule id={}", id);
+    }
+
+    @Override
+    public List<AclUserVO> findUsers() {
+        return new ArrayList<>(users.values());
+    }
+
+    @Override
+    public AclUserVO saveUser(AclUserVO user) {
+        users.put(user.getId(), user);
+        log.debug("Saved ACL user id={}", user.getId());
+        return user;
+    }
+
+    @Override
+    public void deleteUser(String id) {
+        users.remove(id);
+        log.debug("Deleted ACL user id={}", id);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQController.java b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQController.java
new file mode 100644
index 0000000..4cef343
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQController.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.dlq;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/dlq")
+@RequiredArgsConstructor
+public class DLQController {
+
+    private final DLQService dlqService;
+
+    @GetMapping
+    public Result<List<DLQGroupVO>> listDLQGroups(@RequestParam(required = false) String clusterId) {
+        return Result.ok(dlqService.listDLQGroups(clusterId));
+    }
+
+    @PostMapping("/resend")
+    public Result<Void> resendMessages(@RequestBody Map<String, Object> request) {
+        String groupName = (String) request.get("groupName");
+        Long startTime = request.get("startTime") != null ? ((Number) request.get("startTime")).longValue() : null;
+        Long endTime = request.get("endTime") != null ? ((Number) request.get("endTime")).longValue() : null;
+        String targetTopic = (String) request.get("targetTopic");
+        dlqService.resendMessages(groupName, startTime, endTime, targetTopic);
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQGroupVO.java b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQGroupVO.java
new file mode 100644
index 0000000..9782b75
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQGroupVO.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rocketmq.studio.instance.dlq;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DLQGroupVO {
+    private String groupName;
+    private String dlqTopic;  // format: %DLQ%{groupName}
+    private long messageCount;
+    private LocalDateTime lastEnqueueTime;
+    private int retryCount;
+    private String status;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQProvider.java b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQProvider.java
new file mode 100644
index 0000000..45f940c
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQProvider.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.instance.dlq;
+
+
+import java.util.List;
+
+public interface DLQProvider {
+    List<DLQGroupVO> listDLQGroups(String clusterId);
+    void resendMessages(String groupName, Long startTime, Long endTime, String targetTopic);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQProviderStub.java b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQProviderStub.java
new file mode 100644
index 0000000..76b48ba
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQProviderStub.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.rocketmq.studio.instance.dlq;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Collections;
+import java.util.List;
+
+@Component
+@Slf4j
+public class DLQProviderStub implements DLQProvider {
+
+    @Override
+    public List<DLQGroupVO> listDLQGroups(String clusterId) {
+        log.warn("DLQProviderStub.listDLQGroups called - returning empty list");
+        return Collections.emptyList();
+    }
+
+    @Override
+    public void resendMessages(String groupName, Long startTime, Long endTime, String targetTopic) {
+        log.warn("DLQProviderStub.resendMessages called - no-op. group={}, targetTopic={}", groupName, targetTopic);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQService.java b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQService.java
new file mode 100644
index 0000000..f2c0037
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/dlq/DLQService.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.dlq;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class DLQService {
+
+    private final DLQProvider dlqProvider;
+
+    public List<DLQGroupVO> listDLQGroups(String clusterId) {
+        log.info("Listing DLQ groups for cluster: {}", clusterId);
+        return dlqProvider.listDLQGroups(clusterId);
+    }
+
+    public void resendMessages(String groupName, Long startTime, Long endTime, String targetTopic) {
+        log.info("Resending DLQ messages: group={}, targetTopic={}", groupName, targetTopic);
+        dlqProvider.resendMessages(groupName, startTime, endTime, targetTopic);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerGroupController.java b/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerGroupController.java
new file mode 100644
index 0000000..395744f
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerGroupController.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.group;
+
+import com.rocketmq.studio.instance.topic.MetadataService;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/groups")
+@RequiredArgsConstructor
+public class ConsumerGroupController {
+
+    private final MetadataService metadataService;
+
+    @GetMapping
+    public Result<List<ConsumerGroupVO>> listConsumerGroups(
+            @RequestParam(required = false) String clusterId,
+            @RequestParam(required = false) String search) {
+        return Result.ok(metadataService.listConsumerGroups(clusterId, search));
+    }
+
+    @GetMapping("/{name}")
+    public Result<ConsumerGroupVO> getConsumerGroup(@PathVariable String name) {
+        return Result.ok(metadataService.getConsumerGroup(name));
+    }
+
+    @GetMapping("/{name}/progress")
+    public Result<List<QueueProgressVO>> getGroupProgress(@PathVariable String name) {
+        return Result.ok(metadataService.getGroupProgress(name));
+    }
+
+    @GetMapping("/{name}/subscriptions")
+    public Result<List<SubscriptionEntryVO>> getGroupSubscriptions(@PathVariable String name) {
+        return Result.ok(metadataService.getGroupSubscriptions(name));
+    }
+
+    @PostMapping("/create")
+    public Result<ConsumerGroupVO> createConsumerGroup(@RequestBody ConsumerGroupVO group) {
+        return Result.ok(metadataService.createConsumerGroup(group));
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> deleteConsumerGroup(@RequestBody Map<String, String> request) {
+        metadataService.deleteConsumerGroup(request.get("name"));
+        return Result.ok();
+    }
+
+    @PostMapping("/reset-offset")
+    public Result<Void> resetOffset(@RequestBody Map<String, Object> request) {
+        String name = (String) request.get("name");
+        long timestamp = ((Number) request.get("timestamp")).longValue();
+        String topic = (String) request.get("topic");
+        metadataService.resetOffset(name, timestamp, topic);
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerGroupVO.java b/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerGroupVO.java
new file mode 100644
index 0000000..88d283e
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerGroupVO.java
@@ -0,0 +1,43 @@
+/*
+ * 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 com.rocketmq.studio.instance.group;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import com.rocketmq.studio.common.domain.enums.ConsumeType;
+import com.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ConsumerGroupVO extends BaseEntity {
+    private String name;
+    private String namespace;
+    private String clusterId;
+    private SubscriptionMode subscriptionMode;
+    private ConsumeType consumeType;
+    private int onlineInstances;
+    private long totalLag;
+    private List<String> subscribedTopics;
+    private String subscriptionDataType;
+    private String deliveryOrderType;
+    private int retryMaxTimes;
+    private int delaySeconds;
+    private List<ConsumerInstanceVO> instances;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerInstanceVO.java b/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerInstanceVO.java
new file mode 100644
index 0000000..9f2acae
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/group/ConsumerInstanceVO.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.rocketmq.studio.instance.group;
+
+import com.rocketmq.studio.common.domain.enums.Protocol;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ConsumerInstanceVO {
+    private String clientId;
+    private Protocol protocol;
+    private String address;
+    private List<String> subscribedTopics;
+    private LocalDateTime lastHeartbeat;
+    private Map<String, Long> topicLag;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/group/QueueProgressVO.java b/server/src/main/java/com/rocketmq/studio/instance/group/QueueProgressVO.java
new file mode 100644
index 0000000..dd29e26
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/group/QueueProgressVO.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.rocketmq.studio.instance.group;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class QueueProgressVO {
+    private String broker;
+    private int queueId;
+    private long brokerOffset;
+    private long consumerOffset;
+    private long diffTotal;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/group/SubscriptionEntryVO.java b/server/src/main/java/com/rocketmq/studio/instance/group/SubscriptionEntryVO.java
new file mode 100644
index 0000000..b77241c
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/group/SubscriptionEntryVO.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.rocketmq.studio.instance.group;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SubscriptionEntryVO {
+    private String topic;
+    private String expression;
+    private String type;
+    private String filterMode;
+    private String consistency;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/ConsumerStatusVO.java b/server/src/main/java/com/rocketmq/studio/instance/message/ConsumerStatusVO.java
new file mode 100644
index 0000000..a84d2c1
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/ConsumerStatusVO.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.rocketmq.studio.instance.message;
+
+import com.rocketmq.studio.common.domain.enums.DeliveryStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ConsumerStatusVO {
+    private String group;
+    private DeliveryStatus deliveryStatus;
+    private long consumeTime;
+    private int retryCount;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/MessageController.java b/server/src/main/java/com/rocketmq/studio/instance/message/MessageController.java
new file mode 100644
index 0000000..657e85c
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/MessageController.java
@@ -0,0 +1,50 @@
+/*
+ * 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 com.rocketmq.studio.instance.message;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/messages")
+@RequiredArgsConstructor
+public class MessageController {
+
+    private final MessageService messageService;
+
+    @GetMapping
+    public Result<List<MessageRecordVO>> queryMessages(
+            @RequestParam(required = false) String topic,
+            @RequestParam(required = false) String msgId,
+            @RequestParam(required = false) String key,
+            @RequestParam(required = false) Long startTime,
+            @RequestParam(required = false) Long endTime) {
+        return Result.ok(messageService.queryMessages(topic, msgId, key, startTime, endTime));
+    }
+
+    @GetMapping("/{msgId}/trace")
+    public Result<TraceRecordVO> getMessageTrace(@PathVariable String msgId) {
+        return Result.ok(messageService.getMessageTrace(msgId));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/MessageProvider.java b/server/src/main/java/com/rocketmq/studio/instance/message/MessageProvider.java
new file mode 100644
index 0000000..14afe46
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/MessageProvider.java
@@ -0,0 +1,26 @@
+/*
+ * 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 com.rocketmq.studio.instance.message;
+
+
+import java.util.List;
+
+public interface MessageProvider {
+    List<MessageRecordVO> queryMessages(String topic, String msgId, String key, Long startTime, Long endTime);
+
+    TraceRecordVO getMessageTrace(String msgId);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/MessageProviderStub.java b/server/src/main/java/com/rocketmq/studio/instance/message/MessageProviderStub.java
new file mode 100644
index 0000000..cdeb7a7
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/MessageProviderStub.java
@@ -0,0 +1,43 @@
+/*
+ * 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 com.rocketmq.studio.instance.message;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Collections;
+import java.util.List;
+
+@Component
+@Slf4j
+public class MessageProviderStub implements MessageProvider {
+
+    @Override
+    public List<MessageRecordVO> queryMessages(String topic, String msgId, String key, Long startTime, Long endTime) {
+        log.warn("MessageProviderStub.queryMessages called - returning empty list");
+        return Collections.emptyList();
+    }
+
+    @Override
+    public TraceRecordVO getMessageTrace(String msgId) {
+        log.warn("MessageProviderStub.getMessageTrace called - returning empty trace");
+        return TraceRecordVO.builder()
+                .nodes(Collections.emptyList())
+                .consumerStatus(Collections.emptyList())
+                .build();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/MessageRecordVO.java b/server/src/main/java/com/rocketmq/studio/instance/message/MessageRecordVO.java
new file mode 100644
index 0000000..17a44b4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/MessageRecordVO.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MessageRecordVO {
+    private String msgId;
+    private String topic;
+    private String tag;
+    private String key;
+    private String body;
+    private long storeTime;
+    private String bornHost;
+    private String storeHost;
+    private Map<String, String> properties;
+    private int size;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/MessageService.java b/server/src/main/java/com/rocketmq/studio/instance/message/MessageService.java
new file mode 100644
index 0000000..1f3c500
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/MessageService.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.message;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class MessageService {
+
+    private final MessageProvider messageProvider;
+
+    public List<MessageRecordVO> queryMessages(String topic, String msgId, String key, Long startTime, Long endTime) {
+        log.info("Querying messages: topic={}, msgId={}, key={}", topic, msgId, key);
+        return messageProvider.queryMessages(topic, msgId, key, startTime, endTime);
+    }
+
+    public TraceRecordVO getMessageTrace(String msgId) {
+        log.info("Getting message trace: msgId={}", msgId);
+        return messageProvider.getMessageTrace(msgId);
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/TraceNodeVO.java b/server/src/main/java/com/rocketmq/studio/instance/message/TraceNodeVO.java
new file mode 100644
index 0000000..b1926bc
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/TraceNodeVO.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.rocketmq.studio.instance.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TraceNodeVO {
+    private String title;
+    private long timestamp;
+    private String status;
+    private long costTime;
+    private String description;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/message/TraceRecordVO.java b/server/src/main/java/com/rocketmq/studio/instance/message/TraceRecordVO.java
new file mode 100644
index 0000000..e9e7f29
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/message/TraceRecordVO.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.rocketmq.studio.instance.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TraceRecordVO {
+    private List<TraceNodeVO> nodes;
+    private List<ConsumerStatusVO> consumerStatus;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/AdminClient.java b/server/src/main/java/com/rocketmq/studio/instance/topic/AdminClient.java
new file mode 100644
index 0000000..bc85263
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/AdminClient.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.instance.group.ConsumerGroupVO;
+
+
+public interface AdminClient {
+    TopicVO getTopic(String name);
+    ConsumerGroupVO getConsumerGroup(String name);
+    TopicVO createTopic(TopicVO topic);
+    TopicVO updateTopic(TopicVO topic);
+    void deleteTopic(String name);
+    SendMessageVO sendMessage(SendMessageDTO request);
+    ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group);
+    void deleteConsumerGroup(String name);
+    void resetOffset(String name, long timestamp, String topic);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/BrokerRouteVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/BrokerRouteVO.java
new file mode 100644
index 0000000..e17bbe4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/BrokerRouteVO.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.common.domain.enums.TopicPerm;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BrokerRouteVO {
+    private String brokerName;
+    private String brokerAddr;
+    private int writeQueues;
+    private int readQueues;
+    private TopicPerm perm;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/CloudMetadataProvider.java b/server/src/main/java/com/rocketmq/studio/instance/topic/CloudMetadataProvider.java
new file mode 100644
index 0000000..c8e4b78
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/CloudMetadataProvider.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.instance.group.ConsumerGroupVO;
+import com.rocketmq.studio.instance.group.QueueProgressVO;
+import com.rocketmq.studio.instance.group.SubscriptionEntryVO;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Slf4j
+@Component
+public class CloudMetadataProvider implements MetadataProvider {
+
+    @Override
+    public List<TopicVO> listTopics(String clusterId, String type, String search) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public List<ConsumerGroupVO> listConsumerGroups(String clusterId, String search) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public List<BrokerRouteVO> getTopicRoutes(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public List<TopicConsumerVO> getTopicConsumers(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public List<QueueProgressVO> getGroupProgress(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public List<SubscriptionEntryVO> getGroupSubscriptions(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicVO.java
new file mode 100644
index 0000000..8748673
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/LiteTopicVO.java
@@ -0,0 +1,26 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import lombok.Data;
+
+@Data
+public class LiteTopicVO {
+    private String name;
+    private String namespace;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/MetadataProvider.java b/server/src/main/java/com/rocketmq/studio/instance/topic/MetadataProvider.java
new file mode 100644
index 0000000..d0c04d5
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/MetadataProvider.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.instance.group.ConsumerGroupVO;
+import com.rocketmq.studio.instance.group.QueueProgressVO;
+import com.rocketmq.studio.instance.group.SubscriptionEntryVO;
+
+import java.util.List;
+
+public interface MetadataProvider {
+    List<TopicVO> listTopics(String clusterId, String type, String search);
+    List<ConsumerGroupVO> listConsumerGroups(String clusterId, String search);
+    List<BrokerRouteVO> getTopicRoutes(String name);
+    List<TopicConsumerVO> getTopicConsumers(String name);
+    List<QueueProgressVO> getGroupProgress(String name);
+    List<SubscriptionEntryVO> getGroupSubscriptions(String name);
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/MetadataService.java b/server/src/main/java/com/rocketmq/studio/instance/topic/MetadataService.java
new file mode 100644
index 0000000..5518068
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/MetadataService.java
@@ -0,0 +1,117 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.instance.group.ConsumerGroupVO;
+import com.rocketmq.studio.instance.group.QueueProgressVO;
+import com.rocketmq.studio.instance.group.SubscriptionEntryVO;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MetadataService {
+
+    private final MetadataProvider metadataProvider;
+    private final AdminClient adminClient;
+
+    // ── TopicVO ───────────────────────────────────────────────────────
+
+
+    public List<TopicVO> listTopics(String clusterId, String type, String search) {
+        return metadataProvider.listTopics(clusterId, type, search);
+    }
+
+
+    public TopicVO createTopic(TopicVO topic) {
+        return adminClient.createTopic(topic);
+    }
+
+
+    public TopicVO updateTopic(TopicVO topic) {
+        return adminClient.updateTopic(topic);
+    }
+
+
+    public void deleteTopic(String name) {
+        adminClient.deleteTopic(name);
+    }
+
+
+    public List<BrokerRouteVO> getTopicRoutes(String name) {
+        return metadataProvider.getTopicRoutes(name);
+    }
+
+
+    public List<TopicConsumerVO> getTopicConsumers(String name) {
+        return metadataProvider.getTopicConsumers(name);
+    }
+
+
+    public SendMessageVO sendMessage(SendMessageDTO request) {
+        return adminClient.sendMessage(request);
+    }
+
+    // ── ConsumerGroupVO ───────────────────────────────────────────────
+
+
+    public List<ConsumerGroupVO> listConsumerGroups(String clusterId, String search) {
+        return metadataProvider.listConsumerGroups(clusterId, search);
+    }
+
+
+    public ConsumerGroupVO getConsumerGroup(String name) {
+        return adminClient.getConsumerGroup(name);
+    }
+
+
+    public List<QueueProgressVO> getGroupProgress(String name) {
+        return metadataProvider.getGroupProgress(name);
+    }
+
+
+    public List<SubscriptionEntryVO> getGroupSubscriptions(String name) {
+        return metadataProvider.getGroupSubscriptions(name);
+    }
+
+
+    public ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group) {
+        return adminClient.createConsumerGroup(group);
+    }
+
+
+    public void deleteConsumerGroup(String name) {
+        adminClient.deleteConsumerGroup(name);
+    }
+
+
+    public void resetOffset(String name, long timestamp, String topic) {
+        adminClient.resetOffset(name, timestamp, topic);
+    }
+
+    // ── NamespaceVO ───────────────────────────────────────────────────
+
+
+    public List<NamespaceVO> listNamespaces() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/NameSrvAdminClient.java b/server/src/main/java/com/rocketmq/studio/instance/topic/NameSrvAdminClient.java
new file mode 100644
index 0000000..f306afc
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/NameSrvAdminClient.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.instance.group.ConsumerGroupVO;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class NameSrvAdminClient implements AdminClient {
+
+    @Override
+    public TopicVO getTopic(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public ConsumerGroupVO getConsumerGroup(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public TopicVO createTopic(TopicVO topic) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public TopicVO updateTopic(TopicVO topic) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public void deleteTopic(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public SendMessageVO sendMessage(SendMessageDTO request) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public void deleteConsumerGroup(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public void resetOffset(String name, long timestamp, String topic) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/NamespaceController.java b/server/src/main/java/com/rocketmq/studio/instance/topic/NamespaceController.java
new file mode 100644
index 0000000..06c1753
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/NamespaceController.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/namespaces")
+@RequiredArgsConstructor
+public class NamespaceController {
+
+    private final MetadataService metadataService;
+
+    @GetMapping
+    public Result<List<NamespaceVO>> listNamespaces() {
+        return Result.ok(metadataService.listNamespaces());
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/NamespaceVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/NamespaceVO.java
new file mode 100644
index 0000000..061cb24
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/NamespaceVO.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class NamespaceVO extends BaseEntity {
+    private String name;
+    private String clusterId;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/ProxyAdminClient.java b/server/src/main/java/com/rocketmq/studio/instance/topic/ProxyAdminClient.java
new file mode 100644
index 0000000..acc1641
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/ProxyAdminClient.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class ProxyAdminClient {
+    // TODO: Implement proxy admin client for RocketMQ 5.x proxy mode
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/SendMessageDTO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/SendMessageDTO.java
new file mode 100644
index 0000000..ed1d799
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/SendMessageDTO.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SendMessageDTO {
+    private String topic;
+    private String tag;
+    private String key;
+    private String body;
+    private Map<String, String> properties;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/SendMessageVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/SendMessageVO.java
new file mode 100644
index 0000000..51ebad3
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/SendMessageVO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SendMessageVO {
+    private String msgId;
+    private long sendTime;
+    private String offsetMsgId;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/TopicConsumerVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/TopicConsumerVO.java
new file mode 100644
index 0000000..4de25b4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/TopicConsumerVO.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.common.domain.enums.ConsumeType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TopicConsumerVO {
+    private String group;
+    private ConsumeType consumeType;
+    private String messageModel;
+    private double consumeTps;
+    private long diffTotal;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/TopicController.java b/server/src/main/java/com/rocketmq/studio/instance/topic/TopicController.java
new file mode 100644
index 0000000..486bbed
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/TopicController.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/topics")
+@RequiredArgsConstructor
+public class TopicController {
+
+    private final MetadataService metadataService;
+
+    @GetMapping
+    public Result<List<TopicVO>> listTopics(
+            @RequestParam(required = false) String clusterId,
+            @RequestParam(required = false) String type,
+            @RequestParam(required = false) String search) {
+        return Result.ok(metadataService.listTopics(clusterId, type, search));
+    }
+
+    @PostMapping("/create")
+    public Result<TopicVO> createTopic(@RequestBody TopicVO topic) {
+        return Result.ok(metadataService.createTopic(topic));
+    }
+
+    @PostMapping("/update")
+    public Result<TopicVO> updateTopic(@RequestBody TopicVO topic) {
+        return Result.ok(metadataService.updateTopic(topic));
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> deleteTopic(@RequestBody Map<String, String> request) {
+        metadataService.deleteTopic(request.get("name"));
+        return Result.ok();
+    }
+
+    @GetMapping("/{name}/routes")
+    public Result<List<BrokerRouteVO>> getTopicRoutes(@PathVariable String name) {
+        return Result.ok(metadataService.getTopicRoutes(name));
+    }
+
+    @GetMapping("/{name}/consumers")
+    public Result<List<TopicConsumerVO>> getTopicConsumers(@PathVariable String name) {
+        return Result.ok(metadataService.getTopicConsumers(name));
+    }
+
+    @PostMapping("/send")
+    public Result<SendMessageVO> sendMessage(@RequestBody SendMessageDTO request) {
+        return Result.ok(metadataService.sendMessage(request));
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/instance/topic/TopicVO.java b/server/src/main/java/com/rocketmq/studio/instance/topic/TopicVO.java
new file mode 100644
index 0000000..fde407e
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/instance/topic/TopicVO.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.common.domain.BaseEntity;
+import com.rocketmq.studio.common.domain.enums.TopicPerm;
+import com.rocketmq.studio.common.domain.enums.TopicType;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TopicVO extends BaseEntity {
+    private String name;
+    private String namespace;
+    private String clusterId;
+    private TopicType type;
+    private int writeQueues;
+    private int readQueues;
+    private TopicPerm perm;
+    private long messageCount;
+    private double tps;
+    private int consumerGroupCount;
+    private String remark;
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/InstanceControllerTest.java b/server/src/test/java/com/rocketmq/studio/instance/InstanceControllerTest.java
new file mode 100644
index 0000000..e4400b1
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/InstanceControllerTest.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.instance;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.rocketmq.studio.common.domain.enums.InstanceType;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(InstanceController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class InstanceControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private InstanceService instanceService;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Test
+    void listInstancesShouldReturnAllInstances() throws Exception {
+        InstanceVO inst = buildInstance("inst-1", "production-proxy", InstanceType.PROXY, "10.0.1.1:8080");
+
+        when(instanceService.listInstances(isNull(), isNull())).thenReturn(List.of(inst));
+
+        mockMvc.perform(get("/api/instances"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data[0].id").value("inst-1"))
+                .andExpect(jsonPath("$.data[0].name").value("production-proxy"))
+                .andExpect(jsonPath("$.data[0].type").value("PROXY"))
+                .andExpect(jsonPath("$.data[0].endpoint").value("10.0.1.1:8080"));
+    }
+
+    @Test
+    void listInstancesShouldFilterByType() throws Exception {
+        InstanceVO inst = buildInstance("inst-1", "proxy-1", InstanceType.PROXY, "10.0.1.1:8080");
+
+        when(instanceService.listInstances(eq(InstanceType.PROXY), isNull())).thenReturn(List.of(inst));
+
+        mockMvc.perform(get("/api/instances")
+                        .param("type", "PROXY"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data[0].type").value("PROXY"));
+
+        verify(instanceService).listInstances(eq(InstanceType.PROXY), isNull());
+    }
+
+    @Test
+    void listInstancesShouldFilterBySearch() throws Exception {
+        InstanceVO inst = buildInstance("inst-1", "production", InstanceType.PROXY, "10.0.1.1:8080");
+
+        when(instanceService.listInstances(isNull(), eq("prod"))).thenReturn(List.of(inst));
+
+        mockMvc.perform(get("/api/instances")
+                        .param("search", "prod"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data[0].name").value("production"));
+
+        verify(instanceService).listInstances(isNull(), eq("prod"));
+    }
+
+    @Test
+    void createInstanceShouldReturnCreatedInstance() throws Exception {
+        InstanceVO input = InstanceVO.builder()
+                .name("new-instance")
+                .endpoint("10.0.2.1:8080")
+                .type(InstanceType.DIRECT)
+                .build();
+
+        InstanceVO created = InstanceVO.builder()
+                .name("new-instance")
+                .endpoint("10.0.2.1:8080")
+                .type(InstanceType.DIRECT)
+                .topicCount(0)
+                .consumerGroupCount(0)
+                .build();
+        created.setId("new-id");
+        created.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
+        created.setUpdatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
+
+        when(instanceService.createInstance(any(InstanceVO.class))).thenReturn(created);
+
+        mockMvc.perform(post("/api/instances/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(input)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("new-id"))
+                .andExpect(jsonPath("$.data.name").value("new-instance"))
+                .andExpect(jsonPath("$.data.endpoint").value("10.0.2.1:8080"))
+                .andExpect(jsonPath("$.data.type").value("DIRECT"));
+    }
+
+    @Test
+    void updateInstanceShouldReturnUpdatedInstance() throws Exception {
+        InstanceVO update = InstanceVO.builder()
+                .name("updated-name")
+                .build();
+        update.setId("inst-1");
+
+        InstanceVO updated = InstanceVO.builder()
+                .name("updated-name")
+                .endpoint("10.0.1.1:8080")
+                .type(InstanceType.PROXY)
+                .build();
+        updated.setId("inst-1");
+        updated.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
+        updated.setUpdatedAt(LocalDateTime.of(2026, 7, 8, 12, 0));
+
+        when(instanceService.updateInstance(any(InstanceVO.class))).thenReturn(updated);
+
+        mockMvc.perform(post("/api/instances/update")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(update)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("inst-1"))
+                .andExpect(jsonPath("$.data.name").value("updated-name"));
+    }
+
+    @Test
+    void deleteInstanceShouldReturnSuccess() throws Exception {
+        doNothing().when(instanceService).deleteInstance("inst-1");
+
+        Map<String, String> body = Map.of("id", "inst-1");
+
+        mockMvc.perform(post("/api/instances/delete")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(body)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"));
+
+        verify(instanceService).deleteInstance("inst-1");
+    }
+
+    private InstanceVO buildInstance(String id, String name, InstanceType type, String endpoint) {
+        InstanceVO instance = InstanceVO.builder()
+                .name(name)
+                .type(type)
+                .endpoint(endpoint)
+                .topicCount(10)
+                .consumerGroupCount(5)
+                .build();
+        instance.setId(id);
+        instance.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
+        instance.setUpdatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
+        return instance;
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/InstanceServiceTest.java b/server/src/test/java/com/rocketmq/studio/instance/InstanceServiceTest.java
new file mode 100644
index 0000000..3e47087
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/InstanceServiceTest.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.instance;
+
+import com.rocketmq.studio.common.domain.enums.InstanceType;
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class InstanceServiceTest {
+
+    @Mock
+    private InstanceRepository instanceRepository;
+
+    @InjectMocks
+    private InstanceService instanceService;
+
+    @Test
+    void listInstancesShouldReturnAllWhenNoFilters() {
+        List<InstanceVO> instances = List.of(
+                InstanceVO.builder().name("inst-1").build(),
+                InstanceVO.builder().name("inst-2").build()
+        );
+        when(instanceRepository.findAll()).thenReturn(instances);
+
+        List<InstanceVO> result = instanceService.listInstances(null, null);
+
+        assertThat(result).hasSize(2);
+        verify(instanceRepository).findAll();
+    }
+
+    @Test
+    void listInstancesShouldFilterByType() {
+        List<InstanceVO> instances = List.of(
+                InstanceVO.builder().name("proxy-1").type(InstanceType.PROXY).build()
+        );
+        when(instanceRepository.findByType(InstanceType.PROXY)).thenReturn(instances);
+
+        List<InstanceVO> result = instanceService.listInstances(InstanceType.PROXY, null);
+
+        assertThat(result).hasSize(1);
+        verify(instanceRepository).findByType(InstanceType.PROXY);
+    }
+
+    @Test
+    void listInstancesShouldSearchByKeyword() {
+        List<InstanceVO> instances = List.of(
+                InstanceVO.builder().name("production").build()
+        );
+        when(instanceRepository.search("prod")).thenReturn(instances);
+
+        List<InstanceVO> result = instanceService.listInstances(null, "prod");
+
+        assertThat(result).hasSize(1);
+        verify(instanceRepository).search("prod");
+    }
+
+    @Test
+    void listInstancesShouldFilterByTypeAndSearch() {
+        List<InstanceVO> instances = List.of(
+                InstanceVO.builder().name("production-proxy").type(InstanceType.PROXY).build()
+        );
+        when(instanceRepository.findByTypeAndSearch(InstanceType.PROXY, "prod")).thenReturn(instances);
+
+        List<InstanceVO> result = instanceService.listInstances(InstanceType.PROXY, "prod");
+
+        assertThat(result).hasSize(1);
+        verify(instanceRepository).findByTypeAndSearch(InstanceType.PROXY, "prod");
+    }
+
+    @Test
+    void listInstancesShouldIgnoreBlankSearch() {
+        List<InstanceVO> instances = List.of(InstanceVO.builder().name("inst").build());
+        when(instanceRepository.findAll()).thenReturn(instances);
+
+        List<InstanceVO> result = instanceService.listInstances(null, "   ");
+
+        assertThat(result).hasSize(1);
+        verify(instanceRepository).findAll();
+    }
+
+    @Test
+    void createInstanceShouldSetIdAndTimestamps() {
+        InstanceVO input = InstanceVO.builder()
+                .name("new-instance")
+                .endpoint("10.0.1.1:8080")
+                .type(InstanceType.PROXY)
+                .build();
+
+        when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(inv -> inv.getArgument(0));
+
+        InstanceVO result = instanceService.createInstance(input);
+
+        assertThat(result.getId()).isNotBlank();
+        assertThat(result.getCreatedAt()).isNotNull();
+        assertThat(result.getUpdatedAt()).isNotNull();
+        assertThat(result.getName()).isEqualTo("new-instance");
+        verify(instanceRepository).save(any(InstanceVO.class));
+    }
+
+    @Test
+    void createInstanceShouldThrowWhenNameIsNull() {
+        InstanceVO input = InstanceVO.builder()
+                .name(null)
+                .endpoint("10.0.1.1:8080")
+                .build();
+
+        assertThatThrownBy(() -> instanceService.createInstance(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO name is required");
+    }
+
+    @Test
+    void createInstanceShouldThrowWhenNameIsBlank() {
+        InstanceVO input = InstanceVO.builder()
+                .name("  ")
+                .endpoint("10.0.1.1:8080")
+                .build();
+
+        assertThatThrownBy(() -> instanceService.createInstance(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO name is required");
+    }
+
+    @Test
+    void createInstanceShouldThrowWhenEndpointIsNull() {
+        InstanceVO input = InstanceVO.builder()
+                .name("valid-name")
+                .endpoint(null)
+                .build();
+
+        assertThatThrownBy(() -> instanceService.createInstance(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO endpoint is required");
+    }
+
+    @Test
+    void createInstanceShouldThrowWhenEndpointIsBlank() {
+        InstanceVO input = InstanceVO.builder()
+                .name("valid-name")
+                .endpoint("  ")
+                .build();
+
+        assertThatThrownBy(() -> instanceService.createInstance(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO endpoint is required");
+    }
+
+    @Test
+    void updateInstanceShouldMergeFieldsOntoExisting() {
+        InstanceVO existing = InstanceVO.builder()
+                .name("old-name")
+                .endpoint("10.0.1.1:8080")
+                .type(InstanceType.PROXY)
+                .remark("old remark")
+                .build();
+        existing.setId("inst-1");
+
+        InstanceVO update = InstanceVO.builder()
+                .name("new-name")
+                .remark("new remark")
+                .build();
+        update.setId("inst-1");
+
+        when(instanceRepository.findById("inst-1")).thenReturn(Optional.of(existing));
+        when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(inv -> inv.getArgument(0));
+
+        InstanceVO result = instanceService.updateInstance(update);
+
+        assertThat(result.getName()).isEqualTo("new-name");
+        assertThat(result.getEndpoint()).isEqualTo("10.0.1.1:8080");
+        assertThat(result.getType()).isEqualTo(InstanceType.PROXY);
+        assertThat(result.getRemark()).isEqualTo("new remark");
+        assertThat(result.getUpdatedAt()).isNotNull();
+    }
+
+    @Test
+    void updateInstanceShouldThrowWhenIdIsNull() {
+        InstanceVO input = InstanceVO.builder().name("test").build();
+        input.setId(null);
+
+        assertThatThrownBy(() -> instanceService.updateInstance(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO ID is required");
+    }
+
+    @Test
+    void updateInstanceShouldThrowWhenIdIsBlank() {
+        InstanceVO input = InstanceVO.builder().name("test").build();
+        input.setId("  ");
+
+        assertThatThrownBy(() -> instanceService.updateInstance(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO ID is required");
+    }
+
+    @Test
+    void updateInstanceShouldThrowWhenInstanceNotFound() {
+        InstanceVO input = InstanceVO.builder().name("test").build();
+        input.setId("nonexistent");
+
+        when(instanceRepository.findById("nonexistent")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> instanceService.updateInstance(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO not found: nonexistent");
+    }
+
+    @Test
+    void deleteInstanceShouldRemoveExistingInstance() {
+        InstanceVO existing = InstanceVO.builder().name("to-delete").build();
+        existing.setId("inst-1");
+
+        when(instanceRepository.findById("inst-1")).thenReturn(Optional.of(existing));
+
+        instanceService.deleteInstance("inst-1");
+
+        verify(instanceRepository).deleteById("inst-1");
+    }
+
+    @Test
+    void deleteInstanceShouldThrowWhenIdIsNull() {
+        assertThatThrownBy(() -> instanceService.deleteInstance(null))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO ID is required");
+    }
+
+    @Test
+    void deleteInstanceShouldThrowWhenIdIsBlank() {
+        assertThatThrownBy(() -> instanceService.deleteInstance("   "))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO ID is required");
+    }
+
+    @Test
+    void deleteInstanceShouldThrowWhenInstanceNotFound() {
+        when(instanceRepository.findById("missing")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> instanceService.deleteInstance("missing"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("InstanceVO not found: missing");
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/acl/AclControllerTest.java b/server/src/test/java/com/rocketmq/studio/instance/acl/AclControllerTest.java
new file mode 100644
index 0000000..1525082
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/acl/AclControllerTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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 com.rocketmq.studio.instance.acl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(AclController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class AclControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private AclService aclService;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Test
+    void listRulesShouldReturnRules() throws Exception {
+        AclRuleVO rule = AclRuleVO.builder()
+                .principal("user1")
+                .resource("topic-1")
+                .resourceType("TOPIC")
+                .decision("ALLOW")
+                .build();
+        rule.setId("rule-1");
+        rule.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
+
+        when(aclService.listRules(isNull(), isNull())).thenReturn(List.of(rule));
+
+        mockMvc.perform(get("/api/acl/rules"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data[0].id").value("rule-1"))
+                .andExpect(jsonPath("$.data[0].principal").value("user1"))
+                .andExpect(jsonPath("$.data[0].decision").value("ALLOW"));
+    }
+
+    @Test
+    void listRulesShouldPassQueryParams() throws Exception {
+        when(aclService.listRules(eq("cluster-1"), eq("user1"))).thenReturn(List.of());
+
+        mockMvc.perform(get("/api/acl/rules")
+                        .param("clusterId", "cluster-1")
+                        .param("principal", "user1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data").isArray());
+
+        verify(aclService).listRules(eq("cluster-1"), eq("user1"));
+    }
+
+    @Test
+    void createRuleShouldReturnCreatedRule() throws Exception {
+        AclRuleVO input = AclRuleVO.builder()
+                .principal("user1")
+                .resource("topic-1")
+                .resourceType("TOPIC")
+                .decision("ALLOW")
+                .build();
+
+        AclRuleVO created = AclRuleVO.builder()
+                .principal("user1")
+                .resource("topic-1")
+                .resourceType("TOPIC")
+                .decision("ALLOW")
+                .build();
+        created.setId("new-rule-id");
+        created.setCreatedAt(LocalDateTime.of(2026, 7, 8, 12, 0));
+
+        when(aclService.createRule(any(AclRuleVO.class))).thenReturn(created);
+
+        mockMvc.perform(post("/api/acl/rules/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(input)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("new-rule-id"))
+                .andExpect(jsonPath("$.data.principal").value("user1"));
+    }
+
+    @Test
+    void listUsersShouldReturnAllUsers() throws Exception {
+        AclUserVO user = AclUserVO.builder()
+                .username("admin")
+                .accessKey("ak123")
+                .secretKey("sk456")
+                .admin(true)
+                .build();
+        user.setId("user-1");
+        user.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
+
+        when(aclService.listUsers()).thenReturn(List.of(user));
+
+        mockMvc.perform(get("/api/acl/users"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data[0].id").value("user-1"))
+                .andExpect(jsonPath("$.data[0].username").value("admin"))
+                .andExpect(jsonPath("$.data[0].admin").value(true));
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/acl/AclServiceTest.java b/server/src/test/java/com/rocketmq/studio/instance/acl/AclServiceTest.java
new file mode 100644
index 0000000..c24a7a2
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/acl/AclServiceTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.instance.acl;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AclServiceTest {
+
+    @Mock
+    private AclRepository aclRepository;
+
+    @InjectMocks
+    private AclService aclService;
+
+    @Test
+    void listRulesShouldReturnRulesFromRepository() {
+        List<AclRuleVO> rules = List.of(
+                AclRuleVO.builder().principal("user1").resource("topic-1").decision("ALLOW").build(),
+                AclRuleVO.builder().principal("user2").resource("topic-2").decision("DENY").build()
+        );
+        when(aclRepository.findRules("cluster-1", "user1")).thenReturn(rules);
+
+        List<AclRuleVO> result = aclService.listRules("cluster-1", "user1");
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getPrincipal()).isEqualTo("user1");
+        verify(aclRepository).findRules("cluster-1", "user1");
+    }
+
+    @Test
+    void listRulesShouldPassNullFilters() {
+        when(aclRepository.findRules(null, null)).thenReturn(List.of());
+
+        List<AclRuleVO> result = aclService.listRules(null, null);
+
+        assertThat(result).isEmpty();
+        verify(aclRepository).findRules(null, null);
+    }
+
+    @Test
+    void createRuleShouldSetIdAndTimestamp() {
+        AclRuleVO input = AclRuleVO.builder()
+                .principal("user1")
+                .resource("topic-1")
+                .resourceType("TOPIC")
+                .decision("ALLOW")
+                .build();
+
+        when(aclRepository.saveRule(any(AclRuleVO.class))).thenAnswer(inv -> inv.getArgument(0));
+
+        AclRuleVO result = aclService.createRule(input);
+
+        assertThat(result.getId()).isNotBlank();
+        assertThat(result.getCreatedAt()).isNotNull();
+        assertThat(result.getPrincipal()).isEqualTo("user1");
+        assertThat(result.getResource()).isEqualTo("topic-1");
+        verify(aclRepository).saveRule(any(AclRuleVO.class));
+    }
+
+    @Test
+    void deleteRuleShouldDelegateToRepository() {
+        aclService.deleteRule("rule-1");
+
+        verify(aclRepository).deleteRule("rule-1");
+    }
+
+    @Test
+    void listUsersShouldReturnAllUsers() {
+        List<AclUserVO> users = List.of(
+                AclUserVO.builder().username("admin").admin(true).build(),
+                AclUserVO.builder().username("reader").admin(false).build()
+        );
+        when(aclRepository.findUsers()).thenReturn(users);
+
+        List<AclUserVO> result = aclService.listUsers();
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getUsername()).isEqualTo("admin");
+        verify(aclRepository).findUsers();
+    }
+
+    @Test
+    void createUserShouldGenerateKeysAndSetTimestamp() {
+        AclUserVO input = AclUserVO.builder()
+                .username("newuser")
+                .admin(false)
+                .build();
+
+        when(aclRepository.saveUser(any(AclUserVO.class))).thenAnswer(inv -> inv.getArgument(0));
+
+        AclUserVO result = aclService.createUser(input);
+
+        assertThat(result.getId()).isNotBlank();
+        assertThat(result.getAccessKey()).isNotBlank();
+        assertThat(result.getSecretKey()).isNotBlank();
+        assertThat(result.getAccessKey()).doesNotContain("-");
+        assertThat(result.getSecretKey()).doesNotContain("-");
+        assertThat(result.getCreatedAt()).isNotNull();
+        assertThat(result.getUsername()).isEqualTo("newuser");
+        verify(aclRepository).saveUser(any(AclUserVO.class));
+    }
+
+    @Test
+    void deleteUserShouldDelegateToRepository() {
+        aclService.deleteUser("user-1");
+
+        verify(aclRepository).deleteUser("user-1");
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/dlq/DLQControllerTest.java b/server/src/test/java/com/rocketmq/studio/instance/dlq/DLQControllerTest.java
new file mode 100644
index 0000000..3293ecb
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/dlq/DLQControllerTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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 com.rocketmq.studio.instance.dlq;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(DLQController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class DLQControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private DLQService dlqService;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Test
+    void listDLQGroupsShouldReturnGroups() throws Exception {
+        DLQGroupVO group = DLQGroupVO.builder()
+                .groupName("test-group")
+                .dlqTopic("%DLQ%test-group")
+                .messageCount(10)
+                .lastEnqueueTime(LocalDateTime.of(2026, 7, 8, 10, 0))
+                .retryCount(3)
+                .status("ACTIVE")
+                .build();
+
+        when(dlqService.listDLQGroups(isNull())).thenReturn(List.of(group));
+
+        mockMvc.perform(get("/api/dlq"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data[0].groupName").value("test-group"))
+                .andExpect(jsonPath("$.data[0].dlqTopic").value("%DLQ%test-group"))
+                .andExpect(jsonPath("$.data[0].messageCount").value(10));
+    }
+
+    @Test
+    void listDLQGroupsShouldPassClusterId() throws Exception {
+        when(dlqService.listDLQGroups(eq("cluster-1"))).thenReturn(List.of());
+
+        mockMvc.perform(get("/api/dlq")
+                        .param("clusterId", "cluster-1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data").isArray());
+
+        verify(dlqService).listDLQGroups(eq("cluster-1"));
+    }
+
+    @Test
+    void resendMessagesShouldReturnSuccess() throws Exception {
+        Map<String, Object> body = Map.of(
+                "groupName", "test-group",
+                "startTime", 1000,
+                "endTime", 2000,
+                "targetTopic", "target-topic"
+        );
+
+        mockMvc.perform(post("/api/dlq/resend")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(body)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"));
+
+        verify(dlqService).resendMessages(
+                eq("test-group"), eq(1000L), eq(2000L), eq("target-topic"));
+    }
+
+    @Test
+    void resendMessagesShouldHandleNullTimeRange() throws Exception {
+        Map<String, Object> body = Map.of(
+                "groupName", "test-group",
+                "targetTopic", "target-topic"
+        );
+
+        mockMvc.perform(post("/api/dlq/resend")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(body)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200));
+
+        verify(dlqService).resendMessages(
+                eq("test-group"), isNull(), isNull(), eq("target-topic"));
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/dlq/DLQServiceTest.java b/server/src/test/java/com/rocketmq/studio/instance/dlq/DLQServiceTest.java
new file mode 100644
index 0000000..c1631f7
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/dlq/DLQServiceTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.rocketmq.studio.instance.dlq;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DLQServiceTest {
+
+    @Mock
+    private DLQProvider dlqProvider;
+
+    @InjectMocks
+    private DLQService dlqService;
+
+    @Test
+    void listDLQGroupsShouldReturnGroupsFromProvider() {
+        List<DLQGroupVO> groups = List.of(
+                DLQGroupVO.builder()
+                        .groupName("group-1")
+                        .dlqTopic("%DLQ%group-1")
+                        .messageCount(10)
+                        .status("ACTIVE")
+                        .build(),
+                DLQGroupVO.builder()
+                        .groupName("group-2")
+                        .dlqTopic("%DLQ%group-2")
+                        .messageCount(5)
+                        .status("ACTIVE")
+                        .build()
+        );
+        when(dlqProvider.listDLQGroups("cluster-1")).thenReturn(groups);
+
+        List<DLQGroupVO> result = dlqService.listDLQGroups("cluster-1");
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getGroupName()).isEqualTo("group-1");
+        assertThat(result.get(0).getDlqTopic()).isEqualTo("%DLQ%group-1");
+        verify(dlqProvider).listDLQGroups("cluster-1");
+    }
+
+    @Test
+    void listDLQGroupsShouldReturnEmptyWhenNone() {
+        when(dlqProvider.listDLQGroups("cluster-2")).thenReturn(List.of());
+
+        List<DLQGroupVO> result = dlqService.listDLQGroups("cluster-2");
+
+        assertThat(result).isEmpty();
+        verify(dlqProvider).listDLQGroups("cluster-2");
+    }
+
+    @Test
+    void resendMessagesShouldDelegateToProvider() {
+        dlqService.resendMessages("group-1", 1000L, 2000L, "target-topic");
+
+        verify(dlqProvider).resendMessages("group-1", 1000L, 2000L, "target-topic");
+    }
+
+    @Test
+    void resendMessagesShouldAcceptNullTimeRange() {
+        dlqService.resendMessages("group-1", null, null, "target-topic");
+
+        verify(dlqProvider).resendMessages("group-1", null, null, "target-topic");
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/topic/MetadataServiceTest.java b/server/src/test/java/com/rocketmq/studio/instance/topic/MetadataServiceTest.java
new file mode 100644
index 0000000..9e45d71
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/topic/MetadataServiceTest.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.instance.topic;
+
+import com.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MetadataServiceTest {
+
+    @Mock
+    private MetadataProvider metadataProvider;
+
+    @Mock
+    private AdminClient adminClient;
+
+    @InjectMocks
+    private MetadataService metadataService;
+
+    @Test
+    void listTopicsShouldReturnTopicsFromProvider() {
+        TopicVO topic = new TopicVO();
+        topic.setName("test-topic");
+        topic.setWriteQueues(8);
+        topic.setReadQueues(8);
+
+        when(metadataProvider.listTopics("cluster-1", "NORMAL", null)).thenReturn(List.of(topic));
+
+        List<TopicVO> result = metadataService.listTopics("cluster-1", "NORMAL", null);
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getName()).isEqualTo("test-topic");
+        verify(metadataProvider).listTopics("cluster-1", "NORMAL", null);
+    }
+
+    @Test
+    void listTopicsShouldReturnEmptyWhenNone() {
+        when(metadataProvider.listTopics(null, null, "nonexistent")).thenReturn(List.of());
+
+        List<TopicVO> result = metadataService.listTopics(null, null, "nonexistent");
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void createTopicShouldDelegateToAdminClient() {
+        TopicVO input = new TopicVO();
+        input.setName("new-topic");
+        input.setWriteQueues(8);
+        input.setReadQueues(8);
+
+        TopicVO created = new TopicVO();
+        created.setName("new-topic");
+        created.setWriteQueues(8);
+        created.setReadQueues(8);
+
+        when(adminClient.createTopic(any(TopicVO.class))).thenReturn(created);
+
+        TopicVO result = metadataService.createTopic(input);
+
+        assertThat(result.getName()).isEqualTo("new-topic");
+        verify(adminClient).createTopic(input);
+    }
+
+    @Test
+    void deleteTopicShouldDelegateToAdminClient() {
+        metadataService.deleteTopic("topic-to-delete");
+
+        verify(adminClient).deleteTopic("topic-to-delete");
+    }
+
+    @Test
+    void sendMessageShouldReturnResult() {
+        SendMessageDTO request = SendMessageDTO.builder()
+                .topic("test-topic")
+                .tag("TagA")
+                .body("hello")
+                .build();
+
+        SendMessageVO expectedResult = SendMessageVO.builder()
+                .msgId("msg-001")
+                .sendTime(System.currentTimeMillis())
+                .offsetMsgId("offset-001")
+                .build();
+
+        when(adminClient.sendMessage(request)).thenReturn(expectedResult);
+
+        SendMessageVO result = metadataService.sendMessage(request);
+
+        assertThat(result.getMsgId()).isEqualTo("msg-001");
+        assertThat(result.getOffsetMsgId()).isEqualTo("offset-001");
+        verify(adminClient).sendMessage(request);
+    }
+
+    @Test
+    void listConsumerGroupsShouldReturnGroupsFromProvider() {
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("test-group");
+
+        when(metadataProvider.listConsumerGroups("cluster-1", null)).thenReturn(List.of(group));
+
+        List<ConsumerGroupVO> result = metadataService.listConsumerGroups("cluster-1", null);
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getName()).isEqualTo("test-group");
+        verify(metadataProvider).listConsumerGroups("cluster-1", null);
+    }
+
+    @Test
+    void listConsumerGroupsShouldPassSearchFilter() {
+        when(metadataProvider.listConsumerGroups(null, "order")).thenReturn(List.of());
+
+        List<ConsumerGroupVO> result = metadataService.listConsumerGroups(null, "order");
+
+        assertThat(result).isEmpty();
+        verify(metadataProvider).listConsumerGroups(null, "order");
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/instance/topic/TopicControllerTest.java b/server/src/test/java/com/rocketmq/studio/instance/topic/TopicControllerTest.java
new file mode 100644
index 0000000..7d7ab13
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/instance/topic/TopicControllerTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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 com.rocketmq.studio.instance.topic;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(TopicController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class TopicControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private MetadataService metadataService;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Test
+    void listTopicsShouldReturnTopics() throws Exception {
+        TopicVO topic = new TopicVO();
+        topic.setName("test-topic");
+        topic.setWriteQueues(8);
+        topic.setReadQueues(8);
+
+        when(metadataService.listTopics(isNull(), isNull(), isNull())).thenReturn(List.of(topic));
+
+        mockMvc.perform(get("/api/topics"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data[0].name").value("test-topic"))
+                .andExpect(jsonPath("$.data[0].writeQueues").value(8));
+    }
+
+    @Test
+    void listTopicsShouldPassQueryParams() throws Exception {
+        when(metadataService.listTopics(eq("cluster-1"), eq("NORMAL"), eq("test")))
+                .thenReturn(List.of());
+
+        mockMvc.perform(get("/api/topics")
+                        .param("clusterId", "cluster-1")
+                        .param("type", "NORMAL")
+                        .param("search", "test"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data").isArray());
+
+        verify(metadataService).listTopics(eq("cluster-1"), eq("NORMAL"), eq("test"));
+    }
+
+    @Test
+    void createTopicShouldReturnCreatedTopic() throws Exception {
+        TopicVO input = new TopicVO();
+        input.setName("new-topic");
+        input.setWriteQueues(16);
+        input.setReadQueues(16);
+
+        TopicVO created = new TopicVO();
+        created.setName("new-topic");
+        created.setWriteQueues(16);
+        created.setReadQueues(16);
+
+        when(metadataService.createTopic(any(TopicVO.class))).thenReturn(created);
+
+        mockMvc.perform(post("/api/topics/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(input)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.name").value("new-topic"))
+                .andExpect(jsonPath("$.data.writeQueues").value(16));
+    }
+
+    @Test
+    void sendMessageShouldReturnResult() throws Exception {
+        SendMessageDTO request = SendMessageDTO.builder()
+                .topic("test-topic")
+                .tag("TagA")
+                .body("hello world")
+                .build();
+
+        SendMessageVO result = SendMessageVO.builder()
+                .msgId("msg-001")
+                .sendTime(1720000000000L)
+                .offsetMsgId("offset-001")
+                .build();
+
+        when(metadataService.sendMessage(any(SendMessageDTO.class))).thenReturn(result);
+
+        mockMvc.perform(post("/api/topics/send")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(request)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.msgId").value("msg-001"))
+                .andExpect(jsonPath("$.data.offsetMsgId").value("offset-001"));
+    }
+}


(rocketmq-dashboard) 03/09: feat: implement frontend pages and API services

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit 0c95da2ae08a7af367ae93fa91dec8aa1d6a3bc1
Author: lizhimins <70...@qq.com>
AuthorDate: Fri Jul 3 16:52:00 2026 +0800

    feat: implement frontend pages and API services
    
    - Add all page components (dashboard, cluster, instance, ops, settings, ai)
    - Implement API client layer with axios
    - Add mock data for development
    - Create service layer for business logic
    - Add Zustand state management stores
    - Implement i18n support (Chinese/English)
---
 web/src/api/acl.ts                     |   55 ++
 web/src/api/ai.ts                      |   87 +++
 web/src/api/auth.ts                    |   40 +
 web/src/api/client.ts                  |   57 ++
 web/src/api/cluster.ts                 |  171 +++++
 web/src/api/connections.ts             |   23 +
 web/src/api/instance.ts                |   66 ++
 web/src/api/message.ts                 |   80 ++
 web/src/api/metadata.ts                |  172 +++++
 web/src/api/metrics.ts                 |   58 ++
 web/src/api/ops.ts                     |   86 +++
 web/src/api/settings.ts                |   74 ++
 web/src/components/MiniBar.tsx         |   56 ++
 web/src/components/MiniLine.tsx        |   74 ++
 web/src/components/PageHeader.tsx      |   45 ++
 web/src/components/ResultEmpty.tsx     |   29 +
 web/src/components/StatusBadge.tsx     |   40 +
 web/src/i18n/LangContext.tsx           |   49 ++
 web/src/i18n/translations.ts           |  343 +++++++++
 web/src/mock/acl.ts                    |  273 +++++++
 web/src/mock/alerts.ts                 |  125 ++++
 web/src/mock/audit.ts                  |  201 +++++
 web/src/mock/clients.ts                |  255 +++++++
 web/src/mock/clusters.ts               |  405 ++++++++++
 web/src/mock/consumers.ts              | 1070 +++++++++++++++++++++++++++
 web/src/mock/dashboard.ts              |  155 ++++
 web/src/mock/dlq.ts                    |   92 +++
 web/src/mock/instances.ts              |   76 ++
 web/src/mock/messages.ts               | 1268 ++++++++++++++++++++++++++++++++
 web/src/mock/topics.ts                 |  434 +++++++++++
 web/src/pages/ai/index.tsx             |  630 ++++++++++++++++
 web/src/pages/cluster/certs.tsx        |  298 ++++++++
 web/src/pages/cluster/clients.tsx      |  252 +++++++
 web/src/pages/cluster/index.tsx        |  860 ++++++++++++++++++++++
 web/src/pages/home/dashboard.tsx       |  207 ++++++
 web/src/pages/home/index.tsx           |  415 +++++++++++
 web/src/pages/instance/acl.tsx         |  754 +++++++++++++++++++
 web/src/pages/instance/consumer.tsx    | 1062 ++++++++++++++++++++++++++
 web/src/pages/instance/dlq.tsx         |  311 ++++++++
 web/src/pages/instance/index.tsx       |  439 +++++++++++
 web/src/pages/instance/message.tsx     |  553 ++++++++++++++
 web/src/pages/instance/topic.tsx       |  914 +++++++++++++++++++++++
 web/src/pages/ops/alerts.tsx           |  292 ++++++++
 web/src/pages/ops/audit.tsx            |  237 ++++++
 web/src/pages/ops/systemAlerts.tsx     |  162 ++++
 web/src/pages/settings/index.tsx       |  415 +++++++++++
 web/src/services/aclService.ts         |   72 ++
 web/src/services/clusterService.ts     |   47 ++
 web/src/services/connectionsService.ts |   26 +
 web/src/services/consumerService.ts    |   47 ++
 web/src/services/dashboardService.ts   |   14 +
 web/src/services/instanceService.ts    |   52 ++
 web/src/services/messageService.ts     |   38 +
 web/src/services/opsService.ts         |   59 ++
 web/src/services/topicService.ts       |   94 +++
 web/src/stores/authStore.ts            |   40 +
 web/src/stores/clusterStore.ts         |   39 +
 web/src/utils/format.ts                |   26 +
 58 files changed, 14314 insertions(+)

diff --git a/web/src/api/acl.ts b/web/src/api/acl.ts
new file mode 100644
index 0000000..3bd8cd9
--- /dev/null
+++ b/web/src/api/acl.ts
@@ -0,0 +1,55 @@
+import client from './client';
+
+// Matches mock/acl.ts
+export interface AclRule {
+  id: string;
+  principal: string;
+  resource: string;
+  resourceType: string;
+  resourcePattern: string;
+  actions: string[];
+  decision: string;
+  scope: string;
+  aclVersion: string;
+  createdAt: string;
+}
+
+export interface AclUser {
+  id: string;
+  username: string;
+  accessKey: string;
+  secretKey: string;
+  admin: boolean;
+  clusters: string[];
+  createdAt: string;
+}
+
+export async function listAclRules(params?: {
+  keyword?: string;
+  version?: string;
+  decision?: string;
+}) {
+  const res = await client.get<{ data: AclRule[] }>('/acl/rules', { params });
+  return res.data.data;
+}
+
+export async function createAclRule(data: Partial<AclRule>) {
+  await client.post('/acl/rules/create', data);
+}
+
+export async function deleteAclRule(id: string) {
+  await client.post('/acl/rules/delete', { id });
+}
+
+export async function listAclUsers(params?: { keyword?: string }) {
+  const res = await client.get<{ data: AclUser[] }>('/acl/users', { params });
+  return res.data.data;
+}
+
+export async function createAclUser(data: Partial<AclUser>) {
+  await client.post('/acl/users/create', data);
+}
+
+export async function deleteAclUser(id: string) {
+  await client.post('/acl/users/delete', { id });
+}
diff --git a/web/src/api/ai.ts b/web/src/api/ai.ts
new file mode 100644
index 0000000..f38bb02
--- /dev/null
+++ b/web/src/api/ai.ts
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import client from './client';
+
+// ─── Types ──────────────────────────────────────────────────────
+export interface McpTool {
+  name: string;
+  description: string;
+  parameters: Record<string, unknown>;
+}
+
+export interface AiExecuteRequest {
+  message: string;
+  mode: string;
+  model: string;
+  tools?: string[];
+}
+
+// ─── AI ─────────────────────────────────────────────────────────
+export async function chatStream(
+  data: AiExecuteRequest,
+  onChunk: (text: string) => void,
+  signal?: AbortSignal,
+) {
+  const response = await fetch('/api/ai/chat', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      Authorization: `Bearer ${localStorage.getItem('token') || ''}`,
+    },
+    body: JSON.stringify(data),
+    signal,
+  });
+
+  if (!response.ok || !response.body) {
+    throw new Error(`AI chat failed: ${response.statusText}`);
+  }
+
+  const reader = response.body.getReader();
+  const decoder = new TextDecoder();
+  while (true) {
+    const { done, value } = await reader.read();
+    if (done) break;
+    const chunk = decoder.decode(value, { stream: true });
+    // SSE format: "data: ..."
+    const lines = chunk.split('\n').filter((l) => l.startsWith('data: '));
+    for (const line of lines) {
+      const payload = line.slice(6);
+      if (payload === '[DONE]') return;
+      try {
+        const parsed = JSON.parse(payload);
+        if (parsed.content) onChunk(parsed.content);
+      } catch {
+        // raw text chunk
+        onChunk(payload);
+      }
+    }
+  }
+}
+
+export async function executeAiCommand(data: AiExecuteRequest) {
+  const res = await client.post<{ data: { result: string; toolCalls: unknown[] } }>(
+    '/ai/execute',
+    data,
+  );
+  return res.data.data;
+}
+
+export async function listTools() {
+  const res = await client.get<{ data: McpTool[] }>('/ai/tools');
+  return res.data.data;
+}
diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts
new file mode 100644
index 0000000..5d1530d
--- /dev/null
+++ b/web/src/api/auth.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 client from './client';
+
+// ─── Types ──────────────────────────────────────────────────────
+export interface LoginRequest {
+  username: string;
+  password: string;
+}
+
+export interface LoginResponse {
+  token: string;
+  username: string;
+  role: string;
+}
+
+// ─── Auth ───────────────────────────────────────────────────────
+export async function login(username: string, password: string) {
+  const res = await client.post<{ data: LoginResponse }>('/auth/login', { username, password });
+  return res.data.data;
+}
+
+export async function logout() {
+  await client.post('/auth/logout');
+}
diff --git a/web/src/api/client.ts b/web/src/api/client.ts
new file mode 100644
index 0000000..bb341ad
--- /dev/null
+++ b/web/src/api/client.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 axios from 'axios';
+import { message } from 'antd';
+
+const client = axios.create({
+  baseURL: '/api',
+  timeout: 30000,
+});
+
+// Request interceptor: attach Authorization header
+client.interceptors.request.use(
+  (config) => {
+    const token = localStorage.getItem('token');
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  },
+  (error) => Promise.reject(error),
+);
+
+// Response interceptor: check business code and handle 401
+client.interceptors.response.use(
+  (response) => {
+    const data = response.data;
+    if (data.code !== undefined && data.code !== 0) {
+      message.error(data.message || '请求失败');
+      return Promise.reject(new Error(data.message || '请求失败'));
+    }
+    return response;
+  },
+  (error) => {
+    if (error.response?.status === 401) {
+      localStorage.removeItem('token');
+      window.location.href = '/';
+    }
+    return Promise.reject(error);
+  },
+);
+
+export default client;
diff --git a/web/src/api/cluster.ts b/web/src/api/cluster.ts
new file mode 100644
index 0000000..c7d35ef
--- /dev/null
+++ b/web/src/api/cluster.ts
@@ -0,0 +1,171 @@
+/*
+ * 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 client from './client';
+
+// ─── Types ──────────────────────────────────────────────────────
+export interface ClusterInfo {
+  id: string;
+  name: string;
+  type: string;
+  status: string;
+  version: string;
+  brokers: number;
+  proxies: number;
+  topics: number;
+  groups: number;
+  tpsIn: number;
+  tpsOut: number;
+}
+
+export interface ClusterDetail extends ClusterInfo {
+  brokerList: BrokerInfo[];
+  proxyList: ProxyInfo[];
+  nameServerList: NameServerInfo[];
+  config: ClusterConfig;
+}
+
+export interface BrokerInfo {
+  addr: string;
+  brokerName: string;
+  brokerId: number;
+  clusterName: string;
+  status: string;
+  tpsIn: number;
+  tpsOut: number;
+  diskUsage: number;
+  version: string;
+}
+
+export interface ProxyInfo {
+  addr: string;
+  clusterName: string;
+  status: string;
+  connections: number;
+  grpcPort: number;
+  remotingPort: number;
+}
+
+export interface NameServerInfo {
+  addr: string;
+  clusterName: string;
+  status: string;
+  version: string;
+}
+
+export interface ClusterConfig {
+  flushDiskType: string;
+  autoCreateTopicEnable: boolean;
+  autoCreateSubscriptionGroup: boolean;
+  maxMessageSize: number;
+  fileReservedTime: number;
+  writeQueueNums: number;
+  readQueueNums: number;
+  brokerPermission: number;
+}
+
+export interface K8sCertInfo {
+  id: string;
+  name: string;
+  namespace: string;
+  cluster: string;
+  type: string;
+  issuer: string;
+  notBefore: string;
+  notAfter: string;
+  status: string;
+  daysRemaining: number;
+  san: string[];
+}
+
+// ─── Cluster ────────────────────────────────────────────────────
+export async function listClusters() {
+  const res = await client.get<{ data: ClusterInfo[] }>('/clusters');
+  return res.data.data;
+}
+
+export async function getCluster(id: string) {
+  const res = await client.get<{ data: ClusterDetail }>(`/clusters/${id}`);
+  return res.data.data;
+}
+
+export async function updateClusterConfig(data: { id: string } & Partial<ClusterConfig>) {
+  await client.post('/clusters/config/update', data);
+}
+
+export async function restartBroker(clusterId: string, brokerName: string) {
+  const res = await client.post<{ data: { success: boolean; message: string } }>(
+    `/clusters/${clusterId}/brokers/${brokerName}/restart`,
+  );
+  return res.data.data;
+}
+
+// ─── NameServer ─────────────────────────────────────────────────
+export async function restartNameServer(data: { clusterId: string; addr: string }) {
+  await client.post('/nameservers/restart', data);
+}
+
+export async function upgradeNameServer(data: {
+  clusterId: string;
+  addr: string;
+  version: string;
+}) {
+  await client.post('/nameservers/upgrade', data);
+}
+
+export async function deleteNameServer(data: { clusterId: string; addr: string }) {
+  await client.post('/nameservers/delete', data);
+}
+
+export async function createNameServer(data: { clusterId: string; addr: string }) {
+  await client.post('/nameservers/create', data);
+}
+
+export async function updateNameServer(data: {
+  clusterId: string;
+  addr: string;
+  newAddr?: string;
+}) {
+  await client.post('/nameservers/update', data);
+}
+
+// ─── Proxy ──────────────────────────────────────────────────────
+export async function restartProxy(data: { clusterId: string; addr: string }) {
+  await client.post('/proxies/restart', data);
+}
+
+// ─── K8s Certs ──────────────────────────────────────────────────
+export async function listK8sCerts() {
+  const res = await client.get<{ data: K8sCertInfo[] }>('/k8s-certs');
+  return res.data.data;
+}
+
+export async function createK8sCert(data: Partial<K8sCertInfo>) {
+  await client.post('/k8s-certs/create', data);
+}
+
+export async function updateK8sCert(data: Partial<K8sCertInfo>) {
+  await client.post('/k8s-certs/update', data);
+}
+
+export async function renewK8sCert(id: string) {
+  await client.post('/k8s-certs/renew', { id });
+}
+
+export async function deleteK8sCert(id: string) {
+  await client.post('/k8s-certs/delete', { id });
+}
diff --git a/web/src/api/connections.ts b/web/src/api/connections.ts
new file mode 100644
index 0000000..901afad
--- /dev/null
+++ b/web/src/api/connections.ts
@@ -0,0 +1,23 @@
+import client from './client';
+
+// Matches mock/clients.ts
+export interface ClientConnection {
+  clientId: string;
+  type: string;
+  groupOrTopic: string;
+  protocol: string;
+  address: string;
+  language: string;
+  version: string;
+  connectedAt: string;
+  clusterName: string;
+}
+
+export async function listConnections(params?: {
+  keyword?: string;
+  type?: string;
+  language?: string;
+}) {
+  const res = await client.get<{ data: ClientConnection[] }>('/clients', { params });
+  return res.data.data;
+}
diff --git a/web/src/api/instance.ts b/web/src/api/instance.ts
new file mode 100644
index 0000000..30dfe81
--- /dev/null
+++ b/web/src/api/instance.ts
@@ -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 client from './client';
+
+// ─── Types ──────────────────────────────────────────────────────
+export interface Instance {
+  id: string;
+  name: string;
+  remark: string;
+  type: 'PROXY' | 'DIRECT';
+  endpoint: string;
+  topicCount: number;
+  consumerGroupCount: number;
+  createdAt: string;
+  updatedAt: string;
+}
+
+export interface CreateInstanceRequest {
+  name: string;
+  type: 'PROXY' | 'DIRECT';
+  endpoint: string;
+  remark?: string;
+}
+
+export interface UpdateInstanceRequest {
+  id: string;
+  name?: string;
+  type?: 'PROXY' | 'DIRECT';
+  endpoint?: string;
+  remark?: string;
+}
+
+// ─── Instance CRUD ──────────────────────────────────────────────
+export async function listInstances() {
+  const res = await client.get<{ data: Instance[] }>('/instances');
+  return res.data.data;
+}
+
+export async function createInstance(data: CreateInstanceRequest) {
+  const res = await client.post<{ data: Instance }>('/instances/create', data);
+  return res.data.data;
+}
+
+export async function updateInstance(data: UpdateInstanceRequest) {
+  const res = await client.post<{ data: Instance }>('/instances/update', data);
+  return res.data.data;
+}
+
+export async function deleteInstance(id: string) {
+  await client.post('/instances/delete', { id });
+}
diff --git a/web/src/api/message.ts b/web/src/api/message.ts
new file mode 100644
index 0000000..25fdc6a
--- /dev/null
+++ b/web/src/api/message.ts
@@ -0,0 +1,80 @@
+import client from './client';
+
+// Matches mock/messages.ts
+export interface MessageRecord {
+  msgId: string;
+  topic: string;
+  tag: string;
+  key: string;
+  body: string;
+  storeTime: string;
+  bornHost: string;
+  storeHost: string;
+  properties: Record<string, string>;
+  size: number;
+}
+
+export interface TraceNode {
+  title: string;
+  timestamp: string;
+  costTime: number;
+  host: string;
+  status: string;
+  detail?: string;
+}
+
+export interface ConsumerStatus {
+  group: string;
+  clientAddr: string;
+  status: string;
+  consumeTps: number;
+  lastConsumedTime: string;
+}
+
+export interface TraceRecord {
+  nodes: TraceNode[];
+  consumerStatus: ConsumerStatus[];
+}
+
+// Matches mock/dlq.ts
+export interface DLQGroup {
+  groupName: string;
+  dlqTopic: string;
+  messageCount: number;
+  lastEnqueueTime: string;
+  retryCount: number;
+  status: string;
+}
+
+// ─── Messages ───────────────────────────────────────────────────
+export async function queryMessages(params: {
+  mode: string;
+  topic?: string;
+  key?: string;
+  msgId?: string;
+  startTime?: string;
+  endTime?: string;
+}) {
+  const res = await client.get<{ data: MessageRecord[] }>('/messages', { params });
+  return res.data.data;
+}
+
+export async function getMessageTrace(msgId: string) {
+  const res = await client.get<{ data: TraceRecord }>(`/messages/${msgId}/trace`);
+  return res.data.data;
+}
+
+// ─── DLQ ────────────────────────────────────────────────────────
+export async function listDLQGroups() {
+  const res = await client.get<{ data: DLQGroup[] }>('/dlq');
+  return res.data.data;
+}
+
+export async function resendDLQ(data: {
+  groupName: string;
+  startTime: string;
+  endTime: string;
+  targetTopic?: string;
+}) {
+  await client.post('/dlq/resend', data);
+}
diff --git a/web/src/api/metadata.ts b/web/src/api/metadata.ts
new file mode 100644
index 0000000..4c882b1
--- /dev/null
+++ b/web/src/api/metadata.ts
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.
+ */
+
+import client from './client';
+
+// ─── Topic (matches mock/topics.ts) ────────────────────────────
+export interface Topic {
+  name: string;
+  namespace: string;
+  type: string;
+  clusterId: string;
+  writeQueues: number;
+  readQueues: number;
+  perm: string;
+  messageCount: number;
+  tps: number;
+  consumerGroupCount: number;
+  remark: string;
+  createdAt: string;
+  updatedAt: string;
+}
+
+export interface BrokerRoute {
+  brokerName: string;
+  brokerAddr: string;
+  writeQueues: number;
+  readQueues: number;
+  perm: string;
+}
+
+export interface ConsumerGroupInfo {
+  group: string;
+  consumeType: string;
+  messageModel: string;
+  consumeTps: number;
+  diffTotal: number;
+}
+
+// ─── Consumer Group (matches mock/consumers.ts) ─────────────────
+export interface ConsumerGroup {
+  name: string;
+  namespace: string;
+  clusterId: string;
+  subscriptionMode: string;
+  consumeType: string;
+  onlineInstances: number;
+  totalLag: number;
+  subscribedTopics: string[];
+  subscriptionDataType: string;
+  deliveryOrderType?: string;
+  retryMaxTimes: number;
+  createdAt: string;
+}
+
+export interface ConsumerInstance {
+  clientId: string;
+  clientAddr: string;
+  language: string;
+  version: string;
+}
+
+export interface QueueProgress {
+  broker: string;
+  queueId: number;
+  brokerOffset: number;
+  consumerOffset: number;
+  diffTotal: number;
+}
+
+export interface SubscriptionEntry {
+  topic: string;
+  expression: string;
+  type: string;
+  filterMode: string;
+  consistency: string;
+}
+
+// ─── Topic API ──────────────────────────────────────────────────
+export async function listTopics(params?: { keyword?: string; type?: string; namespace?: string }) {
+  const res = await client.get<{ data: Topic[] }>('/topics', { params });
+  return res.data.data;
+}
+
+export async function createTopic(data: Partial<Topic>) {
+  const res = await client.post<{ data: Topic }>('/topics/create', data);
+  return res.data.data;
+}
+
+export async function updateTopic(data: Partial<Topic>) {
+  await client.post('/topics/update', data);
+}
+
+export async function deleteTopic(name: string) {
+  await client.post('/topics/delete', { name });
+}
+
+export async function getTopicRoutes(name: string) {
+  const res = await client.get<{ data: BrokerRoute[] }>(`/topics/${name}/routes`);
+  return res.data.data;
+}
+
+export async function getTopicConsumers(name: string) {
+  const res = await client.get<{ data: ConsumerGroupInfo[] }>(`/topics/${name}/consumers`);
+  return res.data.data;
+}
+
+export interface SendTopicMessageRequest {
+  topic: string;
+  tag?: string;
+  key?: string;
+  body: string;
+  properties?: Record<string, string>;
+}
+
+export interface SendTopicMessageResult {
+  msgId: string;
+  sendTime: string;
+  offsetMsgId: string;
+}
+
+export async function sendTopicMessage(data: SendTopicMessageRequest) {
+  const res = await client.post<{ data: SendTopicMessageResult }>('/topics/send', data);
+  return res.data.data;
+}
+
+// ─── Consumer Group API ─────────────────────────────────────────
+export async function listConsumerGroups(params?: { keyword?: string }) {
+  const res = await client.get<{ data: ConsumerGroup[] }>('/groups', { params });
+  return res.data.data;
+}
+
+export async function getConsumerGroup(name: string) {
+  const res = await client.get<{ data: ConsumerGroup & { instances: ConsumerInstance[] } }>(
+    `/groups/${name}`,
+  );
+  return res.data.data;
+}
+
+export async function getConsumerProgress(name: string) {
+  const res = await client.get<{ data: QueueProgress[] }>(`/groups/${name}/progress`);
+  return res.data.data;
+}
+
+export async function getConsumerSubscriptions(name: string) {
+  const res = await client.get<{ data: SubscriptionEntry[] }>(`/groups/${name}/subscriptions`);
+  return res.data.data;
+}
+
+export async function createConsumerGroup(data: Partial<ConsumerGroup>) {
+  await client.post('/groups/create', data);
+}
+
+export async function deleteConsumerGroup(name: string) {
+  await client.post('/groups/delete', { name });
+}
+
+export async function resetConsumerOffset(data: Record<string, unknown>) {
+  await client.post('/groups/reset-offset', data);
+}
+
+export async function importConsumerGroups(data: string) {
+  await client.post('/groups/import', { data });
+}
+
+export async function exportConsumerGroups(names?: string[]) {
+  const res = await client.get<{ data: string }>('/groups/export', {
+    params: { names: names?.join(',') },
+  });
+  return res.data.data;
+}
diff --git a/web/src/api/metrics.ts b/web/src/api/metrics.ts
new file mode 100644
index 0000000..ddc0cc1
--- /dev/null
+++ b/web/src/api/metrics.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.
+ */
+
+import client from './client';
+
+// ─── Types ──────────────────────────────────────────────────────
+export interface DashboardStats {
+  totalClusters: number;
+  healthyClusters: number;
+  totalBrokers: number;
+  totalProxies: number;
+  totalNameServers: number;
+  totalTopics: number;
+  totalConsumerGroups: number;
+  totalMessagesToday: number;
+  messagesPerSecond: number;
+  tpsIn: number;
+  tpsOut: number;
+}
+
+export interface ClusterOverview {
+  id: string;
+  name: string;
+  type: string;
+  status: string;
+  brokers: number;
+  proxies: number;
+  topics: number;
+  groups: number;
+  tpsIn: number;
+  tpsOut: number;
+  version: string;
+  throughput: number[];
+}
+
+export interface DashboardData {
+  stats: DashboardStats;
+  clusters: ClusterOverview[];
+}
+
+// ─── Dashboard ──────────────────────────────────────────────────
+export async function getDashboard() {
+  const res = await client.get<{ data: DashboardData }>('/dashboard');
+  return res.data.data;
+}
+
+// ─── Metrics ────────────────────────────────────────────────────
+export async function queryMetrics(query: {
+  metric: string;
+  start: number;
+  end: number;
+  step?: string;
+}) {
+  const res = await client.post<{ data: unknown }>('/metrics/query', query);
+  return res.data.data;
+}
diff --git a/web/src/api/ops.ts b/web/src/api/ops.ts
new file mode 100644
index 0000000..8f19996
--- /dev/null
+++ b/web/src/api/ops.ts
@@ -0,0 +1,86 @@
+import client from './client';
+
+// Matches mock/alerts.ts (inferred from data)
+export interface AlertRule {
+  id: string;
+  name: string;
+  metric: string;
+  operator: string;
+  threshold: number;
+  thresholdUnit: string;
+  duration: string;
+  channels: string[];
+  enabled: boolean;
+  lastTriggered: string | null;
+  description: string;
+}
+
+// Matches mock/dashboard.ts systemAlerts
+export interface SystemAlert {
+  id: string;
+  level: string;
+  title: string;
+  description: string;
+  time: string;
+  acknowledged: boolean;
+}
+
+// Matches mock/audit.ts (inferred from data)
+export interface AuditRecord {
+  id: string;
+  timestamp: string;
+  operator: string;
+  operationType: string;
+  target: string;
+  detail: string;
+  ipAddress: string;
+  result: string;
+}
+
+// ─── Alert Rules ────────────────────────────────────────────────
+export async function listAlertRules() {
+  const res = await client.get<{ data: AlertRule[] }>('/alert-rules');
+  return res.data.data;
+}
+
+export async function createAlertRule(data: Partial<AlertRule>) {
+  await client.post('/alert-rules/create', data);
+}
+
+export async function updateAlertRule(data: Partial<AlertRule>) {
+  await client.post('/alert-rules/update', data);
+}
+
+export async function toggleAlertRule(id: string, enabled: boolean) {
+  await client.post('/alert-rules/toggle', { id, enabled });
+}
+
+export async function deleteAlertRule(id: string) {
+  await client.post('/alert-rules/delete', { id });
+}
+
+// ─── System Alerts ──────────────────────────────────────────────
+export async function listSystemAlerts() {
+  const res = await client.get<{ data: SystemAlert[] }>('/system-alerts');
+  return res.data.data;
+}
+
+export async function acknowledgeAlert(id: string) {
+  await client.post('/system-alerts/acknowledge', { id });
+}
+
+export async function clearAcknowledgedAlerts() {
+  await client.post('/system-alerts/clear-acknowledged');
+}
+
+// ─── Audit Logs ─────────────────────────────────────────────────
+export async function listAuditRecords(params?: Record<string, unknown>) {
+  const res = await client.get<{ data: { list: AuditRecord[]; total: number } }>('/audit-logs', {
+    params,
+  });
+  return res.data.data;
+}
+
+export async function cleanupAuditLogs(beforeDays: number) {
+  await client.post('/audit-logs/cleanup', { beforeDays });
+}
diff --git a/web/src/api/settings.ts b/web/src/api/settings.ts
new file mode 100644
index 0000000..8f32b23
--- /dev/null
+++ b/web/src/api/settings.ts
@@ -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.
+ */
+
+import client from './client';
+
+// ─── Types ──────────────────────────────────────────────────────
+export interface GeneralSettings {
+  siteName: string;
+  language: string;
+  theme: string;
+  autoRefreshInterval: number;
+  notificationChannels: string[];
+  llmProvider: string;
+  llmModel: string;
+}
+
+export interface DataSource {
+  id: string;
+  name: string;
+  type: string;
+  url: string;
+  isDefault: boolean;
+  status: string;
+}
+
+// ─── General Settings ───────────────────────────────────────────
+export async function getGeneralSettings() {
+  const res = await client.get<{ data: GeneralSettings }>('/settings/general');
+  return res.data.data;
+}
+
+export async function saveGeneralSettings(data: Partial<GeneralSettings>) {
+  await client.post('/settings/general/save', data);
+}
+
+// ─── Data Sources ───────────────────────────────────────────────
+export async function listDataSources() {
+  const res = await client.get<{ data: DataSource[] }>('/settings/datasources');
+  return res.data.data;
+}
+
+export async function createDataSource(data: Partial<DataSource>) {
+  await client.post('/settings/datasources/create', data);
+}
+
+export async function updateDataSource(data: Partial<DataSource>) {
+  await client.post('/settings/datasources/update', data);
+}
+
+export async function deleteDataSource(id: string) {
+  await client.post('/settings/datasources/delete', { id });
+}
+
+export async function testDataSource(data: { type: string; url: string }) {
+  const res = await client.post<{ data: { success: boolean; message: string } }>(
+    '/settings/datasources/test',
+    data,
+  );
+  return res.data.data;
+}
diff --git a/web/src/components/MiniBar.tsx b/web/src/components/MiniBar.tsx
new file mode 100644
index 0000000..abddcf3
--- /dev/null
+++ b/web/src/components/MiniBar.tsx
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+interface MiniBarProps {
+  data: number[];
+  color?: string;
+  height?: number;
+  width?: number;
+}
+
+const MiniBar = ({ data, color = '#1677ff', height = 32, width = 120 }: MiniBarProps) => {
+  const max = Math.max(...data, 1);
+  const barWidth = Math.max(2, (width - (data.length - 1) * 2) / data.length);
+
+  return (
+    <div
+      style={{
+        display: 'inline-flex',
+        alignItems: 'flex-end',
+        gap: 2,
+        height,
+        width,
+      }}
+    >
+      {data.map((value, i) => (
+        <div
+          key={i}
+          style={{
+            width: barWidth,
+            height: `${Math.max(4, (value / max) * height)}px`,
+            backgroundColor: color,
+            borderRadius: 2,
+            opacity: 0.3 + (i / data.length) * 0.7,
+            transition: 'height 0.3s ease',
+          }}
+        />
+      ))}
+    </div>
+  );
+};
+
+export default MiniBar;
diff --git a/web/src/components/MiniLine.tsx b/web/src/components/MiniLine.tsx
new file mode 100644
index 0000000..8a483ba
--- /dev/null
+++ b/web/src/components/MiniLine.tsx
@@ -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.
+ */
+
+interface MiniLineProps {
+  data: number[];
+  color?: string;
+  height?: number;
+  width?: number;
+  fill?: boolean;
+}
+
+const MiniLine = ({
+  data,
+  color = '#1677ff',
+  height = 32,
+  width = 120,
+  fill = true,
+}: MiniLineProps) => {
+  if (data.length < 2) return null;
+
+  const max = Math.max(...data, 1);
+  const min = Math.min(...data, 0);
+  const range = max - min || 1;
+
+  const padding = 2;
+  const innerW = width - padding * 2;
+  const innerH = height - padding * 2;
+
+  const points = data.map((v, i) => {
+    const x = padding + (i / (data.length - 1)) * innerW;
+    const y = padding + innerH - ((v - min) / range) * innerH;
+    return `${x},${y}`;
+  });
+
+  const linePath = `M${points.join(' L')}`;
+  const areaPath = `${linePath} L${padding + innerW},${height - padding} L${padding},${height - padding} Z`;
+
+  return (
+    <svg width={width} height={height} style={{ display: 'block' }}>
+      {fill && <path d={areaPath} fill={color} opacity={0.1} />}
+      <path
+        d={linePath}
+        fill="none"
+        stroke={color}
+        strokeWidth={1.5}
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+      {/* Last point dot */}
+      <circle
+        cx={padding + innerW}
+        cy={padding + innerH - ((data[data.length - 1] - min) / range) * innerH}
+        r={2.5}
+        fill={color}
+      />
+    </svg>
+  );
+};
+
+export default MiniLine;
diff --git a/web/src/components/PageHeader.tsx b/web/src/components/PageHeader.tsx
new file mode 100644
index 0000000..b34d8bc
--- /dev/null
+++ b/web/src/components/PageHeader.tsx
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Flex, Typography } from 'antd';
+import type { ReactNode } from 'react';
+
+const { Title, Text } = Typography;
+
+interface PageHeaderProps {
+  title: string;
+  subtitle?: ReactNode;
+  extra?: ReactNode;
+}
+
+const PageHeader = ({ title, subtitle, extra }: PageHeaderProps) => (
+  <Flex justify="space-between" align="center" style={{ marginBottom: 24 }}>
+    <Flex align="center" gap={12}>
+      <Title level={4} style={{ margin: 0, fontWeight: 600 }}>
+        {title}
+      </Title>
+      {subtitle && (
+        <Text type="secondary" style={{ fontSize: 13 }}>
+          {subtitle}
+        </Text>
+      )}
+    </Flex>
+    {extra && <Flex gap={8}>{extra}</Flex>}
+  </Flex>
+);
+
+export default PageHeader;
diff --git a/web/src/components/ResultEmpty.tsx b/web/src/components/ResultEmpty.tsx
new file mode 100644
index 0000000..4cd7557
--- /dev/null
+++ b/web/src/components/ResultEmpty.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 { Result } from 'antd';
+
+interface ResultEmptyProps {
+  title?: string;
+  subTitle?: string;
+}
+
+const ResultEmpty = ({ title = '暂无数据', subTitle }: ResultEmptyProps) => (
+  <Result status="info" title={title} subTitle={subTitle} />
+);
+
+export default ResultEmpty;
diff --git a/web/src/components/StatusBadge.tsx b/web/src/components/StatusBadge.tsx
new file mode 100644
index 0000000..83922f3
--- /dev/null
+++ b/web/src/components/StatusBadge.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { Badge, Space, Typography } from 'antd';
+import { STATUS_MAP } from '../constants/theme';
+
+const { Text } = Typography;
+
+interface StatusBadgeProps {
+  status: keyof typeof STATUS_MAP;
+  text?: string;
+  showDot?: boolean;
+}
+
+const StatusBadge = ({ status, text, showDot = true }: StatusBadgeProps) => {
+  const config = STATUS_MAP[status] || STATUS_MAP.offline;
+  const label = text || config.label;
+  return (
+    <Space size={4}>
+      {showDot && <Badge color={config.dot} />}
+      <Text style={{ color: config.color, fontSize: 13 }}>{label}</Text>
+    </Space>
+  );
+};
+
+export default StatusBadge;
diff --git a/web/src/i18n/LangContext.tsx b/web/src/i18n/LangContext.tsx
new file mode 100644
index 0000000..7667156
--- /dev/null
+++ b/web/src/i18n/LangContext.tsx
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createContext, useContext, useState, type ReactNode } from 'react';
+import translations, { type Lang } from './translations';
+
+interface LangContextType {
+  lang: Lang;
+  setLang: (lang: Lang) => void;
+  t: (key: string, params?: Record<string, string | number>) => string;
+}
+
+const LangContext = createContext<LangContextType>({
+  lang: 'zh',
+  setLang: () => {},
+  t: (key) => key,
+});
+
+export const LangProvider = ({ children }: { children: ReactNode }) => {
+  const [lang, setLang] = useState<Lang>('zh');
+
+  const t = (key: string, params?: Record<string, string | number>): string => {
+    let text = translations[key]?.[lang] ?? key;
+    if (params) {
+      Object.entries(params).forEach(([k, v]) => {
+        text = text.replace(`{${k}}`, String(v));
+      });
+    }
+    return text;
+  };
+
+  return <LangContext.Provider value={{ lang, setLang, t }}>{children}</LangContext.Provider>;
+};
+
+export const useLang = () => useContext(LangContext);
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
new file mode 100644
index 0000000..1916eb0
--- /dev/null
+++ b/web/src/i18n/translations.ts
@@ -0,0 +1,343 @@
+/*
+ * 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.
+ */
+
+export type Lang = 'zh' | 'en';
+
+const translations: Record<string, Record<Lang, string>> = {
+  // ─── Navigation ───
+  'nav.home': { zh: '首页', en: 'Home' },
+  'nav.dashboard': { zh: '监控面板', en: 'Dashboard' },
+  'nav.instance': { zh: '实例管理', en: 'Instance' },
+  'nav.instanceList': { zh: '实例列表', en: 'Instance List' },
+  'nav.topic': { zh: 'Topic 管理', en: 'Topic Management' },
+  'nav.group': { zh: 'Group 管理', en: 'Group Management' },
+  'nav.acl': { zh: 'ACL 管理', en: 'ACL Management' },
+  'nav.message': { zh: '消息查询', en: 'Message Search' },
+  'nav.dlq': { zh: '死信队列', en: 'Dead Letter Queue' },
+  'nav.clusterOps': { zh: '集群运维', en: 'Cluster & Ops' },
+  'nav.certs': { zh: 'K8s 证书管理', en: 'K8s Certificates' },
+  'nav.rocketmqCluster': { zh: 'RocketMQ 集群', en: 'RocketMQ Cluster' },
+  'nav.clients': { zh: '客户端连接', en: 'Client Connections' },
+  'nav.alertEvents': { zh: '告警事件', en: 'Alert Events' },
+  'nav.alertRules': { zh: '告警规则', en: 'Alert Rules' },
+  'nav.audit': { zh: '审计日志', en: 'Audit Log' },
+  'nav.ai': { zh: 'AI 交互', en: 'AI Chat' },
+  'nav.settings': { zh: '设置', en: 'Settings' },
+
+  // ─── Common ───
+  'common.search': { zh: '搜索...', en: 'Search...' },
+  'common.searchPlaceholder': { zh: '搜索页面、功能...', en: 'Search pages, features...' },
+  'common.create': { zh: '新建', en: 'Create' },
+  'common.edit': { zh: '编辑', en: 'Edit' },
+  'common.delete': { zh: '删除', en: 'Delete' },
+  'common.confirm': { zh: '确认', en: 'Confirm' },
+  'common.cancel': { zh: '取消', en: 'Cancel' },
+  'common.save': { zh: '保存', en: 'Save' },
+  'common.reset': { zh: '重置', en: 'Reset' },
+  'common.viewAll': { zh: '查看全部', en: 'View All' },
+  'common.total': { zh: '共', en: 'Total' },
+  'common.enabled': { zh: '已启用', en: 'Enabled' },
+  'common.disabled': { zh: '已禁用', en: 'Disabled' },
+  'common.status': { zh: '状态', en: 'Status' },
+  'common.name': { zh: '名称', en: 'Name' },
+  'common.type': { zh: '类型', en: 'Type' },
+  'common.version': { zh: '版本', en: 'Version' },
+  'common.address': { zh: '地址', en: 'Address' },
+  'common.actions': { zh: '操作', en: 'Actions' },
+  'common.success': { zh: '成功', en: 'Success' },
+  'common.failure': { zh: '失败', en: 'Failure' },
+  'common.all': { zh: '全部', en: 'All' },
+  'common.import': { zh: '导入', en: 'Import' },
+  'common.export': { zh: '导出', en: 'Export' },
+  'common.detail': { zh: '详情', en: 'Detail' },
+  'common.close': { zh: '关闭', en: 'Close' },
+  'common.noData': { zh: '暂无数据', en: 'No Data' },
+  'common.autoRefresh': { zh: '自动刷新', en: 'Auto Refresh' },
+  'common.liveRefresh': { zh: '实时刷新', en: 'Live Refresh' },
+  'common.yes': { zh: '是', en: 'Yes' },
+  'common.no': { zh: '否', en: 'No' },
+
+  // ─── Dashboard ───
+  'dashboard.title': { zh: '监控面板', en: 'Dashboard' },
+  'dashboard.subtitle': { zh: 'RocketMQ 集群运行概览', en: 'RocketMQ Cluster Overview' },
+  'dashboard.clusters': { zh: '集群数', en: 'Clusters' },
+  'dashboard.topics': { zh: 'Topic 数', en: 'Topics' },
+  'dashboard.tpsIn': { zh: 'TPS In', en: 'TPS In' },
+  'dashboard.tpsOut': { zh: 'TPS Out', en: 'TPS Out' },
+  'dashboard.todayMessages': { zh: '今日消息', en: "Today's Messages" },
+  'dashboard.clusterHealth': { zh: '集群健康概览', en: 'Cluster Health' },
+  'dashboard.clusterName': { zh: '集群名称', en: 'Cluster Name' },
+  'dashboard.broker': { zh: 'Broker', en: 'Broker' },
+  'dashboard.proxy': { zh: 'Proxy', en: 'Proxy' },
+  'dashboard.topic': { zh: 'Topic', en: 'Topic' },
+  'dashboard.group': { zh: 'Group', en: 'Group' },
+  'dashboard.trend': { zh: '趋势', en: 'Trend' },
+  'dashboard.millionMessages': { zh: '约 {n}M 条消息', en: '~{n}M messages' },
+
+  // ─── Cluster Page ───
+  'cluster.title': { zh: 'RocketMQ 集群', en: 'RocketMQ Cluster' },
+  'cluster.nameserver': { zh: 'NameServer 管理', en: 'NameServer' },
+  'cluster.broker': { zh: 'Broker 管理', en: 'Broker' },
+  'cluster.proxy': { zh: 'Proxy 管理', en: 'Proxy' },
+  'cluster.k8sName': { zh: 'K8s 集群名称', en: 'K8s Cluster' },
+  'cluster.brokerName': { zh: 'Broker 名称', en: 'Broker Name' },
+  'cluster.diskUsage': { zh: '磁盘使用', en: 'Disk Usage' },
+  'cluster.proxyAddr': { zh: 'Proxy 地址', en: 'Proxy Address' },
+  'cluster.connections': { zh: '连接数', en: 'Connections' },
+  'cluster.grpcPort': { zh: 'gRPC 端口', en: 'gRPC Port' },
+  'cluster.remotingPort': { zh: 'Remoting 端口', en: 'Remoting Port' },
+  'cluster.config': { zh: '配置', en: 'Config' },
+  'cluster.restart': { zh: '重启', en: 'Restart' },
+  'cluster.upgrade': { zh: '升级', en: 'Upgrade' },
+  'cluster.createCluster': { zh: '新建集群', en: 'New Cluster' },
+  'cluster.createNameServer': { zh: '新建 NameServer', en: 'New NameServer' },
+  'cluster.editNameServer': { zh: '编辑 NameServer', en: 'Edit NameServer' },
+  'cluster.nsAddr': { zh: 'NameServer 地址', en: 'NameServer Address' },
+  'cluster.nsAddrPlaceholder': { zh: '例:10.0.1.1:9876', en: 'e.g. 10.0.1.1:9876' },
+  'cluster.newAddr': { zh: '新地址', en: 'New Address' },
+  'cluster.newAddrPlaceholder': { zh: '不填则不修改', en: 'Leave empty to keep unchanged' },
+  'cluster.selectCluster': { zh: '选择集群', en: 'Select Cluster' },
+  'cluster.nsCreated': { zh: 'NameServer 已创建', en: 'NameServer created' },
+  'cluster.nsUpdated': { zh: 'NameServer 已更新', en: 'NameServer updated' },
+  'cluster.nsClusterName': { zh: 'NameServer 集群名称', en: 'NS Cluster Name' },
+  'cluster.count': { zh: '数量', en: 'Count' },
+  'cluster.searchBroker': { zh: '搜索 Broker 名称或地址', en: 'Search Broker name or address' },
+  'cluster.searchProxy': { zh: '搜索 Proxy 地址', en: 'Search Proxy address' },
+  'cluster.searchNs': { zh: '搜索地址', en: 'Search address' },
+  'cluster.running': { zh: '运行中', en: 'Running' },
+  'cluster.readonly': { zh: '只读', en: 'Read Only' },
+  'cluster.maintenance': { zh: '维护', en: 'Maintenance' },
+  'cluster.healthy': { zh: '运行中', en: 'Healthy' },
+  'cluster.warning': { zh: '告警', en: 'Warning' },
+  'cluster.error': { zh: '异常', en: 'Error' },
+  'cluster.offline': { zh: '离线', en: 'Offline' },
+  'cluster.heartbeat': { zh: '心跳时间', en: 'Heartbeat' },
+  'cluster.restartNotSupported': {
+    zh: '暂不支持在线重启,请通过运维平台操作',
+    en: 'Online restart not supported, please use ops platform',
+  },
+  'cluster.confirmRestart': { zh: '确认重启', en: 'Confirm Restart' },
+
+  // ─── Instance / Topic / Group / ACL / Message ───
+  'instance.title': { zh: '实例列表', en: 'Instance List' },
+  'instance.subtitle': { zh: '管理 RocketMQ 集群连接', en: 'Manage RocketMQ cluster connections' },
+  'instance.count': { zh: '共 {n} 个实例', en: '{n} instances' },
+  'instance.searchPlaceholder': { zh: '搜索实例名称或地址', en: 'Search name or endpoint' },
+  'instance.allTypes': { zh: '全部架构', en: 'All Types' },
+  'instance.proxyMode': { zh: 'Proxy 模式', en: 'Proxy Mode' },
+  'instance.directMode': { zh: 'Direct 模式', en: 'Direct Mode' },
+  'instance.addInstance': { zh: '添加实例', en: 'Add Instance' },
+  'instance.instanceName': { zh: '实例名称', en: 'Instance Name' },
+  'instance.namePlaceholder': { zh: '例:rocketmq-production', en: 'e.g. rocketmq-production' },
+  'instance.accessType': { zh: '接入方式', en: 'Access Type' },
+  'instance.selectAccessType': { zh: '选择接入方式', en: 'Select access type' },
+  'instance.endpoint': { zh: '接入地址', en: 'Endpoint' },
+  'instance.endpointPlaceholder': {
+    zh: '例:proxy.example.com:8080',
+    en: 'e.g. proxy.example.com:8080',
+  },
+  'instance.remark': { zh: '备注', en: 'Remark' },
+  'instance.connect': { zh: '连接', en: 'Connect' },
+  'instance.cancel': { zh: '取消', en: 'Cancel' },
+  'instance.edit': { zh: '编辑', en: 'Edit' },
+  'instance.delete': { zh: '删除', en: 'Delete' },
+  'instance.confirmDelete': { zh: '确认删除 "{name}"?', en: 'Delete "{name}"?' },
+  'instance.deleteWarning': { zh: '此操作不可恢复。', en: 'This cannot be undone.' },
+  'instance.deleted': { zh: '已删除', en: 'Deleted' },
+  'instance.added': { zh: '实例添加成功', en: 'Instance added' },
+  'instance.enter': { zh: '进入 {name}', en: 'Enter {name}' },
+  'instance.createdAt': { zh: '创建时间', en: 'Created' },
+  'instance.updatedAt': { zh: '修改时间', en: 'Updated' },
+  'topic.title': { zh: 'Topic 管理', en: 'Topic Management' },
+  'group.title': { zh: 'Group 管理', en: 'Group Management' },
+  'acl.title': { zh: 'ACL 管理', en: 'ACL Management' },
+  'message.title': { zh: '消息查询', en: 'Message Search' },
+
+  // ─── Dead Letter Queue ───
+  'dlq.title': { zh: '死信队列', en: 'Dead Letter Queue' },
+
+  // ─── Client Connections ───
+  'clients.title': { zh: '客户端连接', en: 'Client Connections' },
+  'clients.clientId': { zh: 'Client ID', en: 'Client ID' },
+  'clients.groupOrTopic': { zh: 'Group/Topic', en: 'Group/Topic' },
+  'clients.protocol': { zh: '协议', en: 'Protocol' },
+  'clients.language': { zh: '语言', en: 'Language' },
+  'clients.cluster': { zh: '所属集群', en: 'Cluster' },
+  'clients.allClusters': { zh: '全部集群', en: 'All Clusters' },
+  'clients.searchPlaceholder': { zh: '搜索 Client ID 或地址', en: 'Search Client ID or address' },
+
+  // ─── Alert Rules ───
+  'alerts.title': { zh: '告警规则管理', en: 'Alert Rules' },
+  'alerts.subtitle': {
+    zh: '配置集群监控告警规则与通知渠道',
+    en: 'Configure cluster monitoring alert rules and notification channels',
+  },
+  'alerts.totalRules': { zh: '规则总数', en: 'Total Rules' },
+  'alerts.enabled': { zh: '已启用', en: 'Enabled' },
+  'alerts.triggered24h': { zh: '24h 触发', en: 'Triggered (24h)' },
+  'alerts.newRule': { zh: '新建规则', en: 'New Rule' },
+  'alerts.ruleName': { zh: '规则名称', en: 'Rule Name' },
+  'alerts.metric': { zh: '监控指标', en: 'Metric' },
+  'alerts.threshold': { zh: '阈值条件', en: 'Threshold' },
+  'alerts.duration': { zh: '持续时间', en: 'Duration' },
+  'alerts.channels': { zh: '通知渠道', en: 'Channels' },
+  'alerts.lastTriggered': { zh: '最近触发', en: 'Last Triggered' },
+  'alerts.neverTriggered': { zh: '从未触发', en: 'Never' },
+  'alerts.ruleCreated': { zh: '规则创建成功', en: 'Rule created successfully' },
+
+  // ─── System Alerts ───
+  'sysAlerts.title': { zh: '系统告警', en: 'System Alerts' },
+  'sysAlerts.clearAcked': { zh: '清除已确认', en: 'Clear Acknowledged' },
+  'sysAlerts.acknowledge': { zh: '确认', en: 'Acknowledge' },
+  'sysAlerts.severe': { zh: '严重', en: 'Critical' },
+  'sysAlerts.warning': { zh: '警告', en: 'Warning' },
+  'sysAlerts.info': { zh: '信息', en: 'Info' },
+  'sysAlerts.noAlerts': { zh: '暂无告警', en: 'No Alerts' },
+  'sysAlerts.acknowledged': { zh: '告警已确认', en: 'Alert Acknowledged' },
+  'sysAlerts.cleared': { zh: '已清除所有已确认告警', en: 'Cleared all acknowledged alerts' },
+  'sysAlerts.subtitle': {
+    zh: '集群运行告警监控,当前 {n} 条未确认',
+    en: 'Cluster alert monitoring, {n} unacknowledged',
+  },
+
+  // ─── Audit Log ───
+  'audit.title': { zh: '运维审计日志', en: 'Audit Log' },
+  'audit.subtitle': {
+    zh: '记录所有管控操作,支持按操作人、类型和时间筛选',
+    en: 'All control operations, filterable by operator, type and time',
+  },
+  'audit.time': { zh: '操作时间', en: 'Time' },
+  'audit.operator': { zh: '操作人', en: 'Operator' },
+  'audit.opType': { zh: '操作类型', en: 'Operation Type' },
+  'audit.target': { zh: '操作对象', en: 'Target' },
+  'audit.detail': { zh: '操作详情', en: 'Detail' },
+  'audit.ip': { zh: 'IP 地址', en: 'IP Address' },
+  'audit.result': { zh: '结果', en: 'Result' },
+  'audit.searchPlaceholder': { zh: '搜索操作人或操作对象', en: 'Search operator or target' },
+  'audit.cleanup': { zh: '清理日志', en: 'Cleanup' },
+  'audit.cleanupTitle': { zh: '清理审计日志', en: 'Cleanup Audit Log' },
+  'audit.cleanupDesc': {
+    zh: '清理指定天数之前的操作日志,此操作不可恢复。',
+    en: 'Clean logs older than specified days. This cannot be undone.',
+  },
+  'audit.cleanupDays': { zh: '清理 {n} 天之前的日志', en: 'Clean logs older than {n} days' },
+  'audit.cleanupConfirm': { zh: '确认清理', en: 'Confirm Cleanup' },
+  'audit.cleanupSuccess': { zh: '已清理 {n} 天之前的日志', en: 'Cleaned logs older than {n} days' },
+
+  // ─── AI Page ───
+  'ai.title': { zh: 'AI 交互', en: 'AI Chat' },
+  'ai.commonCommands': { zh: '常用指令', en: 'Common Commands' },
+
+  // ─── Home Page ───
+  'home.greeting.night': { zh: '夜深了', en: 'Late night' },
+  'home.greeting.morning': { zh: '上午好', en: 'Good morning' },
+  'home.greeting.noon': { zh: '中午好', en: 'Good afternoon' },
+  'home.greeting.afternoon': { zh: '下午好', en: 'Good afternoon' },
+  'home.greeting.evening': { zh: '晚上好', en: 'Good evening' },
+  'home.mode.query': { zh: '消息查询', en: 'Message Query' },
+  'home.mode.diagnose': { zh: '集群诊断', en: 'Cluster Diagnose' },
+  'home.mode.manage': { zh: '资源管理', en: 'Resource Management' },
+  'home.mode.chat': { zh: 'AI 对话', en: 'AI Chat' },
+  'home.recommended': { zh: '推荐', en: 'Recommended' },
+  'home.welcomeTo': { zh: '来到', en: 'to' },
+  'home.tagline': {
+    zh: '跨集群 · 跨架构 · 跨云,统一管控你的 RocketMQ 集群',
+    en: 'Cross-cluster · Cross-arch · Cross-cloud, unified RocketMQ management',
+  },
+
+  // ─── ACL ───
+  'acl.subtitle': {
+    zh: '访问控制规则与用户权限管理,共 {rules} 条规则、{users} 个用户',
+    en: 'Access control rules and user permissions, {rules} rules, {users} users',
+  },
+  'acl.addRule': { zh: '添加规则', en: 'Add Rule' },
+  'acl.addUser': { zh: '添加用户', en: 'Add User' },
+  'acl.ruleTab': { zh: 'ACL 规则', en: 'ACL Rules' },
+  'acl.userTab': { zh: '用户管理', en: 'Users' },
+  'acl.principal': { zh: '主体', en: 'Principal' },
+  'acl.resource': { zh: '资源', en: 'Resource' },
+  'acl.permissions': { zh: '操作权限', en: 'Permissions' },
+  'acl.decision': { zh: '决策', en: 'Decision' },
+  'acl.aclVersion': { zh: 'ACL 版本', en: 'ACL Version' },
+  'acl.scope': { zh: '范围', en: 'Scope' },
+  'acl.createdAt': { zh: '创建时间', en: 'Created' },
+  'acl.allow': { zh: '允许', en: 'Allow' },
+  'acl.deny': { zh: '拒绝', en: 'Deny' },
+  'acl.cluster': { zh: '集群', en: 'Cluster' },
+  'acl.namespace': { zh: '命名空间', en: 'Namespace' },
+  'acl.admin': { zh: '管理员', en: 'Admin' },
+  'acl.username': { zh: '用户名', en: 'Username' },
+  'acl.associatedClusters': { zh: '关联集群', en: 'Associated Clusters' },
+  'acl.searchPrincipal': { zh: '搜索主体或资源', en: 'Search principal or resource' },
+  'acl.allVersions': { zh: '全部版本', en: 'All Versions' },
+  'acl.allDecisions': { zh: '全部决策', en: 'All Decisions' },
+  'acl.totalRules': { zh: '共 {n} 条规则', en: '{n} rules total' },
+  'acl.totalUsers': { zh: '共 {n} 个用户', en: '{n} users total' },
+  'acl.editRule': { zh: '编辑规则', en: 'Edit Rule' },
+  'acl.save': { zh: '保存', en: 'Save' },
+  'acl.add': { zh: '添加', en: 'Add' },
+  'acl.resourceType': { zh: '资源类型', en: 'Resource Type' },
+  'acl.resourceName': { zh: '资源名称', en: 'Resource Name' },
+  'acl.matchPattern': { zh: '匹配模式', en: 'Match Pattern' },
+  'acl.literal': { zh: '精确匹配 (LITERAL)', en: 'Exact (LITERAL)' },
+  'acl.prefix': { zh: '前缀匹配 (PREFIX)', en: 'Prefix (PREFIX)' },
+  'acl.pub': { zh: '发布 (PUB)', en: 'Publish (PUB)' },
+  'acl.sub': { zh: '订阅 (SUB)', en: 'Subscribe (SUB)' },
+  'acl.all': { zh: '全部 (ALL)', en: 'All (ALL)' },
+  'acl.allowDesc': { zh: '允许 (ALLOW)', en: 'Allow (ALLOW)' },
+  'acl.denyDesc': { zh: '拒绝 (DENY)', en: 'Deny (DENY)' },
+  'acl.effectScope': { zh: '生效范围', en: 'Effect Scope' },
+  'acl.editUser': { zh: '编辑用户', en: 'Edit User' },
+  'acl.ruleUpdated': { zh: '规则已更新', en: 'Rule updated' },
+  'acl.ruleAdded': { zh: '规则已添加', en: 'Rule added' },
+  'acl.ruleDeleted': { zh: '规则已删除', en: 'Rule deleted' },
+  'acl.userUpdated': { zh: '用户已更新', en: 'User updated' },
+  'acl.userAdded': { zh: '用户已添加', en: 'User added' },
+  'acl.userDeleted': { zh: '用户已删除', en: 'User deleted' },
+  'acl.adminSet': { zh: '已设为管理员', en: 'Set as admin' },
+  'acl.adminRemoved': { zh: '已取消管理员', en: 'Admin removed' },
+  'acl.confirmDeleteRule': { zh: '确认删除此规则?', en: 'Delete this rule?' },
+  'acl.confirmDeleteUser': { zh: '确认删除此用户?', en: 'Delete this user?' },
+  'acl.deleteWarning': { zh: '删除后无法恢复,请确认。', en: 'This cannot be undone.' },
+  'acl.deleteUserWarning': {
+    zh: '删除用户后,关联的 ACL 规则不受影响。',
+    en: 'Associated ACL rules will not be affected.',
+  },
+  'acl.adminBadge': { zh: '管理员', en: 'Admin' },
+  'acl.selectPrincipal': { zh: '选择用户', en: 'Select user' },
+  'acl.selectResourceType': { zh: '选择资源类型', en: 'Select resource type' },
+  'acl.resourceNamePlaceholder': {
+    zh: '例:order-*、payment-callback、*',
+    en: 'e.g. order-*, payment-callback, *',
+  },
+  'acl.selectEffectScope': { zh: '选择生效范围', en: 'Select effect scope' },
+  'acl.clusterScope': { zh: '集群 (cluster)', en: 'Cluster (cluster)' },
+  'acl.namespaceScope': { zh: '命名空间 (namespace)', en: 'Namespace (namespace)' },
+  'acl.usernamePlaceholder': { zh: '例:user-order-service', en: 'e.g. user-order-service' },
+  'acl.autoOrManual': { zh: '自动生成或手动输入', en: 'Auto-generate or enter manually' },
+  'acl.required': { zh: '请选择{field}', en: 'Please select {field}' },
+  'acl.inputRequired': { zh: '请输入{field}', en: 'Please enter {field}' },
+
+  // ─── Settings ───
+  'settings.title': { zh: '设置', en: 'Settings' },
+
+  // ─── User Menu ───
+  'user.profile': { zh: '个人中心', en: 'Profile' },
+  'user.logout': { zh: '退出登录', en: 'Logout' },
+};
+
+export default translations;
diff --git a/web/src/mock/acl.ts b/web/src/mock/acl.ts
new file mode 100644
index 0000000..615b75e
--- /dev/null
+++ b/web/src/mock/acl.ts
@@ -0,0 +1,273 @@
+/*
+ * 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.
+ */
+
+export interface AclRule {
+  id: string;
+  principal: string;
+  resource: string;
+  resourceType: 'Topic' | 'Group' | 'Cluster';
+  resourcePattern: 'LITERAL' | 'PREFIX';
+  actions: ('PUB' | 'SUB' | 'ALL')[];
+  decision: 'ALLOW' | 'DENY';
+  scope: 'cluster' | 'namespace';
+  aclVersion: '1.0' | '2.0';
+  createdAt: string;
+}
+
+export interface AclUser {
+  id: string;
+  username: string;
+  accessKey: string;
+  secretKey: string;
+  admin: boolean;
+  clusters: string[];
+  createdAt: string;
+}
+
+export const aclRules: AclRule[] = [
+  {
+    id: 'acl-001',
+    principal: 'user-order-service',
+    resource: 'order-*',
+    resourceType: 'Topic',
+    resourcePattern: 'PREFIX',
+    actions: ['PUB', 'SUB'],
+    decision: 'ALLOW',
+    scope: 'cluster',
+    aclVersion: '2.0',
+    createdAt: '2025-08-12T10:30:00Z',
+  },
+  {
+    id: 'acl-002',
+    principal: 'user-payment-service',
+    resource: 'payment-*',
+    resourceType: 'Topic',
+    resourcePattern: 'PREFIX',
+    actions: ['PUB', 'SUB'],
+    decision: 'ALLOW',
+    scope: 'cluster',
+    aclVersion: '2.0',
+    createdAt: '2025-09-05T14:20:00Z',
+  },
+  {
+    id: 'acl-003',
+    principal: 'user-admin',
+    resource: '*',
+    resourceType: 'Cluster',
+    resourcePattern: 'LITERAL',
+    actions: ['ALL'],
+    decision: 'ALLOW',
+    scope: 'cluster',
+    aclVersion: '2.0',
+    createdAt: '2025-03-01T08:00:00Z',
+  },
+  {
+    id: 'acl-004',
+    principal: 'user-log-collector',
+    resource: 'system-log',
+    resourceType: 'Topic',
+    resourcePattern: 'LITERAL',
+    actions: ['SUB'],
+    decision: 'ALLOW',
+    scope: 'namespace',
+    aclVersion: '1.0',
+    createdAt: '2025-06-20T16:45:00Z',
+  },
+  {
+    id: 'acl-005',
+    principal: 'user-order-service',
+    resource: 'cg-order-*',
+    resourceType: 'Group',
+    resourcePattern: 'PREFIX',
+    actions: ['SUB'],
+    decision: 'ALLOW',
+    scope: 'cluster',
+    aclVersion: '2.0',
+    createdAt: '2025-08-15T11:00:00Z',
+  },
+  {
+    id: 'acl-006',
+    principal: 'user-inventory-service',
+    resource: 'inventory-sync',
+    resourceType: 'Topic',
+    resourcePattern: 'LITERAL',
+    actions: ['PUB', 'SUB'],
+    decision: 'ALLOW',
+    scope: 'cluster',
+    aclVersion: '2.0',
+    createdAt: '2025-10-10T09:30:00Z',
+  },
+  {
+    id: 'acl-007',
+    principal: 'user-guest',
+    resource: 'payment-callback',
+    resourceType: 'Topic',
+    resourcePattern: 'LITERAL',
+    actions: ['PUB', 'SUB'],
+    decision: 'DENY',
+    scope: 'cluster',
+    aclVersion: '1.0',
+    createdAt: '2025-11-02T13:15:00Z',
+  },
+  {
+    id: 'acl-008',
+    principal: 'user-notification-service',
+    resource: 'notification-*',
+    resourceType: 'Topic',
+    resourcePattern: 'PREFIX',
+    actions: ['PUB'],
+    decision: 'ALLOW',
+    scope: 'namespace',
+    aclVersion: '2.0',
+    createdAt: '2025-12-18T10:00:00Z',
+  },
+  {
+    id: 'acl-009',
+    principal: 'user-risk-control',
+    resource: 'order-create',
+    resourceType: 'Topic',
+    resourcePattern: 'LITERAL',
+    actions: ['SUB'],
+    decision: 'ALLOW',
+    scope: 'cluster',
+    aclVersion: '1.0',
+    createdAt: '2026-01-08T15:30:00Z',
+  },
+  {
+    id: 'acl-010',
+    principal: 'user-guest',
+    resource: '*',
+    resourceType: 'Cluster',
+    resourcePattern: 'LITERAL',
+    actions: ['PUB'],
+    decision: 'DENY',
+    scope: 'cluster',
+    aclVersion: '2.0',
+    createdAt: '2026-02-14T08:45:00Z',
+  },
+  {
+    id: 'acl-011',
+    principal: 'user-payment-service',
+    resource: 'cg-payment-*',
+    resourceType: 'Group',
+    resourcePattern: 'PREFIX',
+    actions: ['SUB'],
+    decision: 'ALLOW',
+    scope: 'cluster',
+    aclVersion: '2.0',
+    createdAt: '2026-03-22T11:20:00Z',
+  },
+  {
+    id: 'acl-012',
+    principal: 'user-monitor',
+    resource: 'user-activity-log',
+    resourceType: 'Topic',
+    resourcePattern: 'LITERAL',
+    actions: ['SUB'],
+    decision: 'ALLOW',
+    scope: 'namespace',
+    aclVersion: '1.0',
+    createdAt: '2026-04-15T09:10:00Z',
+  },
+  {
+    id: 'acl-013',
+    principal: 'user-ai-service',
+    resource: 'chat/sess-*',
+    resourceType: 'Topic',
+    resourcePattern: 'PREFIX',
+    actions: ['PUB', 'SUB'],
+    decision: 'ALLOW',
+    scope: 'namespace',
+    aclVersion: '2.0',
+    createdAt: '2026-05-20T14:00:00Z',
+  },
+];
+
+export const aclUsers: AclUser[] = [
+  {
+    id: 'u-001',
+    username: 'user-admin',
+    accessKey: 'LTAI****admin',
+    secretKey: 'HqWz****xK8P',
+    admin: true,
+    clusters: ['rmq-cn-v5-prod-01', 'rmq-cn-v5-prod-02', 'rmq-cn-v4-prod-02'],
+    createdAt: '2025-01-10T08:00:00Z',
+  },
+  {
+    id: 'u-002',
+    username: 'user-order-service',
+    accessKey: 'LTAI****ordr',
+    secretKey: 'MnBv****7dF3',
+    admin: false,
+    clusters: ['rmq-cn-v5-prod-01'],
+    createdAt: '2025-03-22T10:30:00Z',
+  },
+  {
+    id: 'u-003',
+    username: 'user-payment-service',
+    accessKey: 'LTAI****paym',
+    secretKey: 'XcZa****9pQ2',
+    admin: false,
+    clusters: ['rmq-cn-v5-prod-01', 'rmq-cn-v5-prod-02'],
+    createdAt: '2025-04-15T14:20:00Z',
+  },
+  {
+    id: 'u-004',
+    username: 'user-log-collector',
+    accessKey: 'LTAI****logs',
+    secretKey: 'RtYu****3hJ5',
+    admin: false,
+    clusters: ['rmq-cn-v4-prod-02'],
+    createdAt: '2025-06-01T09:00:00Z',
+  },
+  {
+    id: 'u-005',
+    username: 'user-guest',
+    accessKey: 'LTAI****guest',
+    secretKey: 'KpLm****6wN8',
+    admin: false,
+    clusters: ['rmq-cn-v5-prod-01'],
+    createdAt: '2025-07-18T16:00:00Z',
+  },
+  {
+    id: 'u-006',
+    username: 'user-inventory-service',
+    accessKey: 'LTAI****invn',
+    secretKey: 'GhJk****4tY1',
+    admin: false,
+    clusters: ['rmq-cn-v5-prod-01', 'rmq-cn-v5-prod-02'],
+    createdAt: '2025-09-10T11:30:00Z',
+  },
+  {
+    id: 'u-007',
+    username: 'user-notification-service',
+    accessKey: 'LTAI****ntfy',
+    secretKey: 'BvCx****8mZ6',
+    admin: false,
+    clusters: ['rmq-cn-v5-prod-01'],
+    createdAt: '2025-11-05T13:45:00Z',
+  },
+  {
+    id: 'u-008',
+    username: 'user-monitor',
+    accessKey: 'LTAI****monr',
+    secretKey: 'WqEr****2fA4',
+    admin: false,
+    clusters: ['rmq-cn-v5-prod-01', 'rmq-cn-v5-prod-02', 'rmq-cn-v4-prod-02'],
+    createdAt: '2026-01-20T10:00:00Z',
+  },
+];
diff --git a/web/src/mock/alerts.ts b/web/src/mock/alerts.ts
new file mode 100644
index 0000000..294ff46
--- /dev/null
+++ b/web/src/mock/alerts.ts
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+export const mockAlertRules = [
+  {
+    id: 'alert-001',
+    name: 'Broker 磁盘使用率过高',
+    metric: '磁盘使用率',
+    operator: '>' as const,
+    threshold: 85,
+    thresholdUnit: '%',
+    duration: '5分钟',
+    channels: ['dingtalk', 'email'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: true,
+    lastTriggered: '2026-07-02 18:32:11',
+    description: '当 Broker 磁盘使用率超过 85% 时触发告警',
+  },
+  {
+    id: 'alert-002',
+    name: '消费堆积量异常',
+    metric: '消费堆积量',
+    operator: '>' as const,
+    threshold: 10000,
+    thresholdUnit: '条',
+    duration: '15分钟',
+    channels: ['dingtalk', 'sms'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: true,
+    lastTriggered: '2026-07-02 14:05:23',
+    description: '消费组堆积消息数超过 10000 条时触发',
+  },
+  {
+    id: 'alert-003',
+    name: 'TPS 突降告警',
+    metric: 'TPS 异常',
+    operator: '<' as const,
+    threshold: 500,
+    thresholdUnit: 'TPS',
+    duration: '5分钟',
+    channels: ['dingtalk', 'email', 'sms'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: true,
+    lastTriggered: '2026-07-01 22:17:45',
+    description: 'TPS 低于 500 时触发,可能表示生产端异常',
+  },
+  {
+    id: 'alert-004',
+    name: 'Broker 离线检测',
+    metric: 'Broker 离线',
+    operator: '>=' as const,
+    threshold: 1,
+    thresholdUnit: '个',
+    duration: '1分钟',
+    channels: ['dingtalk', 'sms'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: true,
+    lastTriggered: '2026-06-30 09:12:08',
+    description: '检测到任意 Broker 节点离线时立即触发',
+  },
+  {
+    id: 'alert-005',
+    name: 'Proxy 连接数过高',
+    metric: 'Proxy 连接数',
+    operator: '>' as const,
+    threshold: 5000,
+    thresholdUnit: '个',
+    duration: '5分钟',
+    channels: ['dingtalk', 'email'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: false,
+    lastTriggered: '2026-06-28 11:45:33',
+    description: 'Proxy 连接数超过 5000 时触发告警',
+  },
+  {
+    id: 'alert-006',
+    name: '磁盘使用率预警',
+    metric: '磁盘使用率',
+    operator: '>=' as const,
+    threshold: 70,
+    thresholdUnit: '%',
+    duration: '30分钟',
+    channels: ['email'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: true,
+    lastTriggered: null,
+    description: '磁盘使用率超过 70% 的预警级别',
+  },
+  {
+    id: 'alert-007',
+    name: '消费堆积轻微告警',
+    metric: '消费堆积量',
+    operator: '>' as const,
+    threshold: 5000,
+    thresholdUnit: '条',
+    duration: '30分钟',
+    channels: ['dingtalk'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: true,
+    lastTriggered: '2026-07-03 08:21:17',
+    description: '消费堆积量超过 5000 条时的轻微告警',
+  },
+  {
+    id: 'alert-008',
+    name: 'TPS 突增告警',
+    metric: 'TPS 异常',
+    operator: '>' as const,
+    threshold: 50000,
+    thresholdUnit: 'TPS',
+    duration: '1分钟',
+    channels: ['dingtalk', 'email'] as ('dingtalk' | 'email' | 'sms')[],
+    enabled: false,
+    lastTriggered: '2026-06-25 16:08:52',
+    description: 'TPS 超过 50000 时触发,防止流量突增压垮集群',
+  },
+];
+
+export type AlertRule = (typeof mockAlertRules)[number];
diff --git a/web/src/mock/audit.ts b/web/src/mock/audit.ts
new file mode 100644
index 0000000..59f9ab2
--- /dev/null
+++ b/web/src/mock/audit.ts
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const mockAuditRecords = [
+  {
+    id: 'audit-001',
+    timestamp: '2026-07-03 09:45:12',
+    operator: 'admin',
+    operationType: '创建Topic',
+    target: 'rmq-trade-order-topic',
+    detail: '创建 Topic: rmq-trade-order-topic,队列数 16,权限 6',
+    ipAddress: '10.0.12.45',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-002',
+    timestamp: '2026-07-03 09:32:08',
+    operator: 'ops-zhang',
+    operationType: '修改配置',
+    target: 'rmq-payment-group',
+    detail: '修改消费组 rmq-payment-group 的消费模式为 CLUSTERING',
+    ipAddress: '10.0.15.22',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-003',
+    timestamp: '2026-07-03 08:17:55',
+    operator: 'ops-li',
+    operationType: '重置位点',
+    target: 'rmq-log-consumer-group',
+    detail: '重置消费组 rmq-log-consumer-group 的位点到 2026-07-02 00:00:00',
+    ipAddress: '10.0.8.101',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-004',
+    timestamp: '2026-07-03 07:58:33',
+    operator: 'admin',
+    operationType: '删除Topic',
+    target: 'rmq-test-temp-topic',
+    detail: '删除测试用 Topic: rmq-test-temp-topic',
+    ipAddress: '10.0.12.45',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-005',
+    timestamp: '2026-07-03 07:22:19',
+    operator: 'ops-wang',
+    operationType: 'ACL变更',
+    target: 'rmq-secure-topic',
+    detail: '为 Topic rmq-secure-topic 添加 ACL 规则,允许 user: readonly 读取',
+    ipAddress: '10.0.20.88',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-006',
+    timestamp: '2026-07-02 22:45:01',
+    operator: 'system',
+    operationType: '重启Broker',
+    target: 'broker-cn-hangzhou-02',
+    detail: '自动重启异常 Broker 节点 broker-cn-hangzhou-02',
+    ipAddress: '10.0.1.2',
+    result: 'failure' as const,
+  },
+  {
+    id: 'audit-007',
+    timestamp: '2026-07-02 20:11:47',
+    operator: 'ops-zhang',
+    operationType: '修改配置',
+    target: 'broker-cn-shanghai-01',
+    detail: '修改 Broker broker-cn-shanghai-01 的 flushDiskType 为 ASYNC_FLUSH',
+    ipAddress: '10.0.15.22',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-008',
+    timestamp: '2026-07-02 18:33:28',
+    operator: 'ops-li',
+    operationType: '创建Topic',
+    target: 'rmq-notification-topic',
+    detail: '创建 Topic: rmq-notification-topic,队列数 8,权限 6',
+    ipAddress: '10.0.8.101',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-009',
+    timestamp: '2026-07-02 16:05:14',
+    operator: 'admin',
+    operationType: '删除消费组',
+    target: 'rmq-legacy-consumer-group',
+    detail: '删除已废弃的消费组 rmq-legacy-consumer-group',
+    ipAddress: '10.0.12.45',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-010',
+    timestamp: '2026-07-02 14:28:56',
+    operator: 'ops-wang',
+    operationType: 'ACL变更',
+    target: 'rmq-admin-topic',
+    detail: '更新 Topic rmq-admin-topic 的 ACL 规则,移除 user: guest 的访问权限',
+    ipAddress: '10.0.20.88',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-011',
+    timestamp: '2026-07-02 11:52:09',
+    operator: 'ops-zhang',
+    operationType: '重置位点',
+    target: 'rmq-order-consumer-group',
+    detail: '重置消费组 rmq-order-consumer-group 的位点到 2026-07-01 12:00:00',
+    ipAddress: '10.0.15.22',
+    result: 'failure' as const,
+  },
+  {
+    id: 'audit-012',
+    timestamp: '2026-07-02 10:15:42',
+    operator: 'system',
+    operationType: '重启Broker',
+    target: 'broker-cn-beijing-03',
+    detail: '手动重启 Broker 节点 broker-cn-beijing-03 以应用配置变更',
+    ipAddress: '10.0.1.1',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-013',
+    timestamp: '2026-07-01 23:41:18',
+    operator: 'ops-li',
+    operationType: '创建Topic',
+    target: 'rmq-metrics-topic',
+    detail: '创建 Topic: rmq-metrics-topic,队列数 32,权限 6',
+    ipAddress: '10.0.8.101',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-014',
+    timestamp: '2026-07-01 19:08:35',
+    operator: 'admin',
+    operationType: '修改配置',
+    target: 'rmq-cluster-config',
+    detail: '更新集群配置:开启消息轨迹追踪功能',
+    ipAddress: '10.0.12.45',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-015',
+    timestamp: '2026-07-01 15:27:50',
+    operator: 'ops-wang',
+    operationType: '删除Topic',
+    target: 'rmq-deprecated-topic',
+    detail: '删除已废弃 Topic: rmq-deprecated-topic',
+    ipAddress: '10.0.20.88',
+    result: 'failure' as const,
+  },
+  {
+    id: 'audit-016',
+    timestamp: '2026-07-01 13:55:23',
+    operator: 'ops-zhang',
+    operationType: 'ACL变更',
+    target: 'rmq-billing-topic',
+    detail: '为 Topic rmq-billing-topic 创建新的 ACL 规则,限制仅 admin 组可写入',
+    ipAddress: '10.0.15.22',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-017',
+    timestamp: '2026-07-01 10:33:07',
+    operator: 'ops-li',
+    operationType: '删除消费组',
+    target: 'rmq-old-consumer-group',
+    detail: '删除已迁移的旧消费组 rmq-old-consumer-group',
+    ipAddress: '10.0.8.101',
+    result: 'success' as const,
+  },
+  {
+    id: 'audit-018',
+    timestamp: '2026-07-01 08:19:44',
+    operator: 'system',
+    operationType: '重启Broker',
+    target: 'broker-cn-shenzhen-01',
+    detail: '自动重启异常 Broker 节点 broker-cn-shenzhen-01,磁盘 IO 超时',
+    ipAddress: '10.0.1.3',
+    result: 'success' as const,
+  },
+] as const;
+
+export type AuditRecord = (typeof mockAuditRecords)[number];
diff --git a/web/src/mock/clients.ts b/web/src/mock/clients.ts
new file mode 100644
index 0000000..6ece01a
--- /dev/null
+++ b/web/src/mock/clients.ts
@@ -0,0 +1,255 @@
+/*
+ * 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.
+ */
+
+/* ─── Types ─── */
+
+export interface ClientConnection {
+  clientId: string;
+  type: 'Producer' | 'Consumer';
+  groupOrTopic: string;
+  protocol: 'gRPC' | 'Remoting';
+  address: string;
+  language: 'Java' | 'Go' | 'Python' | 'Rust';
+  version: string;
+  connectedAt: string;
+  clusterName: string;
+}
+
+/* ─── Mock Data ─── */
+
+export const mockClients: ClientConnection[] = [
+  {
+    clientId: 'order-svc-0@10.0.1.12:49152',
+    type: 'Producer',
+    groupOrTopic: 'order-create',
+    protocol: 'gRPC',
+    address: '10.0.1.12:49152',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 08:30:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'order-svc-1@10.0.1.13:49200',
+    type: 'Producer',
+    groupOrTopic: 'order-status-change',
+    protocol: 'gRPC',
+    address: '10.0.1.13:49200',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 08:30:05',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'payment-svc-0@10.0.2.10:50100',
+    type: 'Producer',
+    groupOrTopic: 'payment-callback',
+    protocol: 'gRPC',
+    address: '10.0.2.10:50100',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 08:32:10',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'cg-order-notify-0@10.0.1.12:49160',
+    type: 'Consumer',
+    groupOrTopic: 'cg-order-notify',
+    protocol: 'gRPC',
+    address: '10.0.1.12:49160',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 08:31:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'cg-order-notify-1@10.0.1.13:49210',
+    type: 'Consumer',
+    groupOrTopic: 'cg-order-notify',
+    protocol: 'gRPC',
+    address: '10.0.1.13:49210',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 08:31:05',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'cg-payment-cb-0@10.0.2.10:50110',
+    type: 'Consumer',
+    groupOrTopic: 'cg-payment-callback',
+    protocol: 'gRPC',
+    address: '10.0.2.10:50110',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 08:33:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'user-svc-0@10.0.3.20:51100',
+    type: 'Producer',
+    groupOrTopic: 'user-activity-log',
+    protocol: 'Remoting',
+    address: '10.0.3.20:51100',
+    language: 'Go',
+    version: '5.0.3',
+    connectedAt: '2026-07-01 09:00:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'cg-user-act-0@10.0.3.20:51110',
+    type: 'Consumer',
+    groupOrTopic: 'cg-user-activity',
+    protocol: 'Remoting',
+    address: '10.0.3.20:51110',
+    language: 'Go',
+    version: '5.0.3',
+    connectedAt: '2026-07-01 09:00:30',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'log-collector-0@10.0.5.40:53100',
+    type: 'Consumer',
+    groupOrTopic: 'cg-log-collector',
+    protocol: 'gRPC',
+    address: '10.0.5.40:53100',
+    language: 'Rust',
+    version: '5.0.2',
+    connectedAt: '2026-07-01 07:00:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'log-collector-1@10.0.5.41:53200',
+    type: 'Consumer',
+    groupOrTopic: 'cg-log-collector',
+    protocol: 'gRPC',
+    address: '10.0.5.41:53200',
+    language: 'Rust',
+    version: '5.0.2',
+    connectedAt: '2026-07-01 07:00:10',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'notif-gw-0@10.0.6.50:54100',
+    type: 'Producer',
+    groupOrTopic: 'notification-push',
+    protocol: 'gRPC',
+    address: '10.0.6.50:54100',
+    language: 'Python',
+    version: '5.0.1',
+    connectedAt: '2026-07-01 08:45:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'cg-notif-push-0@10.0.6.50:54110',
+    type: 'Consumer',
+    groupOrTopic: 'cg-notification-push',
+    protocol: 'gRPC',
+    address: '10.0.6.50:54110',
+    language: 'Python',
+    version: '5.0.1',
+    connectedAt: '2026-07-01 08:45:30',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'ai-infer-0@10.0.7.60:55100',
+    type: 'Consumer',
+    groupOrTopic: 'cg-ai-task-worker',
+    protocol: 'gRPC',
+    address: '10.0.7.60:55100',
+    language: 'Python',
+    version: '5.0.1',
+    connectedAt: '2026-07-01 10:00:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'ai-dispatch-0@10.0.7.70:55200',
+    type: 'Producer',
+    groupOrTopic: 'ai-task-dispatch',
+    protocol: 'gRPC',
+    address: '10.0.7.70:55200',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 10:00:15',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'metrics-agent-0@10.0.8.70:56100',
+    type: 'Producer',
+    groupOrTopic: 'metrics-raw',
+    protocol: 'Remoting',
+    address: '10.0.8.70:56100',
+    language: 'Go',
+    version: '5.0.3',
+    connectedAt: '2026-07-01 06:00:00',
+    clusterName: 'ns-pre',
+  },
+  {
+    clientId: 'cg-metrics-agg-0@10.0.8.70:56110',
+    type: 'Consumer',
+    groupOrTopic: 'cg-metrics-aggregator',
+    protocol: 'Remoting',
+    address: '10.0.8.70:56110',
+    language: 'Go',
+    version: '5.0.3',
+    connectedAt: '2026-07-01 06:00:30',
+    clusterName: 'ns-pre',
+  },
+  {
+    clientId: 'risk-engine-0@10.0.9.80:57100',
+    type: 'Consumer',
+    groupOrTopic: 'cg-risk-control',
+    protocol: 'gRPC',
+    address: '10.0.9.80:57100',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 08:00:00',
+    clusterName: 'ns-prod',
+  },
+  {
+    clientId: 'binlog-bridge-0@10.0.10.90:58100',
+    type: 'Producer',
+    groupOrTopic: 'binlog-event',
+    protocol: 'gRPC',
+    address: '10.0.10.90:58100',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 06:30:00',
+    clusterName: 'ns-pre',
+  },
+  {
+    clientId: 'cg-data-sync-0@10.0.10.90:58110',
+    type: 'Consumer',
+    groupOrTopic: 'cg-data-sync',
+    protocol: 'gRPC',
+    address: '10.0.10.90:58110',
+    language: 'Java',
+    version: '5.0.7',
+    connectedAt: '2026-07-01 06:30:30',
+    clusterName: 'ns-pre',
+  },
+  {
+    clientId: 'search-idx-0@10.0.11.100:59100',
+    type: 'Consumer',
+    groupOrTopic: 'cg-search-indexer',
+    protocol: 'gRPC',
+    address: '10.0.11.100:59100',
+    language: 'Rust',
+    version: '5.0.2',
+    connectedAt: '2026-07-01 09:15:00',
+    clusterName: 'ns-pre',
+  },
+];
diff --git a/web/src/mock/clusters.ts b/web/src/mock/clusters.ts
new file mode 100644
index 0000000..0e935a9
--- /dev/null
+++ b/web/src/mock/clusters.ts
@@ -0,0 +1,405 @@
+/*
+ * 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.
+ */
+
+export interface BrokerInfo {
+  name: string;
+  addr: string;
+  version: string;
+  status: 'running' | 'readonly' | 'maintenance';
+  diskUsage: number;
+  tpsIn: number;
+  tpsOut: number;
+}
+
+export interface ProxyInfo {
+  addr: string;
+  status: 'healthy' | 'warning' | 'error' | 'offline';
+  connections: number;
+  grpcPort: number;
+  remotingPort: number;
+}
+
+export interface NameServerInfo {
+  addr: string;
+  status: 'healthy' | 'warning' | 'error' | 'offline';
+}
+
+export interface K8sCertInfo {
+  id: string;
+  name: string;
+  namespace: string;
+  cluster: string;
+  type: 'TLS' | 'mTLS' | 'ServiceAccount';
+  issuer: string;
+  notBefore: string;
+  notAfter: string;
+  status: 'valid' | 'expiring' | 'expired';
+  daysRemaining: number;
+  san: string[];
+}
+
+export const mockK8sCerts: K8sCertInfo[] = [
+  {
+    id: 'cert-001',
+    name: 'ca90643d13159433da1dadd826160b2c1',
+    namespace: 'kube-system',
+    cluster: 'rocketmq-prod',
+    type: 'TLS',
+    issuer: 'kubernetes-ca',
+    notBefore: '2025-06-01T00:00:00Z',
+    notAfter: '2026-06-01T00:00:00Z',
+    status: 'valid',
+    daysRemaining: 335,
+    san: ['kubernetes', 'kubernetes.default', 'kubernetes.default.svc'],
+  },
+  {
+    id: 'cert-002',
+    name: 'e7f3a8c291b04d56a1e2f4c6b8d0a9235',
+    namespace: 'kube-system',
+    cluster: 'rocketmq-prod',
+    type: 'mTLS',
+    issuer: 'etcd-ca',
+    notBefore: '2025-06-01T00:00:00Z',
+    notAfter: '2026-06-01T00:00:00Z',
+    status: 'valid',
+    daysRemaining: 335,
+    san: ['etcd-0', 'etcd-1', 'etcd-2'],
+  },
+  {
+    id: 'cert-003',
+    name: 'b2d4f6a8c0e2f4a6b8d0c2e4f6a8b0d2',
+    namespace: 'cert-manager',
+    cluster: 'rocketmq-prod',
+    type: 'TLS',
+    issuer: 'letsencrypt-prod',
+    notBefore: '2025-01-15T00:00:00Z',
+    notAfter: '2025-07-15T00:00:00Z',
+    status: 'expiring',
+    daysRemaining: 12,
+    san: ['*.rocketmq-prod.example.com', 'rocketmq-prod.example.com'],
+  },
+  {
+    id: 'cert-004',
+    name: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6',
+    namespace: 'kube-system',
+    cluster: 'rocketmq-pre',
+    type: 'TLS',
+    issuer: 'kubernetes-ca',
+    notBefore: '2025-09-01T00:00:00Z',
+    notAfter: '2026-09-01T00:00:00Z',
+    status: 'valid',
+    daysRemaining: 427,
+    san: ['kubernetes', 'kubernetes.default', 'kubernetes.default.svc'],
+  },
+  {
+    id: 'cert-005',
+    name: 'f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4',
+    namespace: 'istio-system',
+    cluster: 'rocketmq-pre',
+    type: 'mTLS',
+    issuer: 'istio-ca',
+    notBefore: '2025-09-01T00:00:00Z',
+    notAfter: '2026-09-01T00:00:00Z',
+    status: 'valid',
+    daysRemaining: 427,
+    san: ['*.rocketmq-pre.svc.cluster.local', 'istiod.istio-system.svc'],
+  },
+  {
+    id: 'cert-006',
+    name: 'c0ffee1bad2bad3caffe4bad5coffee6bad7',
+    namespace: 'kube-system',
+    cluster: 'rocketmq-prod',
+    type: 'TLS',
+    issuer: 'self-signed',
+    notBefore: '2025-03-01T00:00:00Z',
+    notAfter: '2025-06-20T00:00:00Z',
+    status: 'expired',
+    daysRemaining: -10,
+    san: ['kubernetes', 'localhost'],
+  },
+  {
+    id: 'cert-007',
+    name: 'd3adb33fd3adb33fd3adb33fd3adb33f',
+    namespace: 'kube-system',
+    cluster: 'rocketmq-pre',
+    type: 'ServiceAccount',
+    issuer: 'k8s-sa',
+    notBefore: '2026-01-01T00:00:00Z',
+    notAfter: '2027-01-01T00:00:00Z',
+    status: 'valid',
+    daysRemaining: 549,
+    san: ['system:serviceaccount:rocketmq:broker-sa'],
+  },
+  {
+    id: 'cert-008',
+    name: '5ca1ab1e5ca1ab1e5ca1ab1e5ca1ab1e',
+    namespace: 'kube-system',
+    cluster: 'rocketmq-prod',
+    type: 'mTLS',
+    issuer: 'vault-pki',
+    notBefore: '2025-12-01T00:00:00Z',
+    notAfter: '2026-12-01T00:00:00Z',
+    status: 'valid',
+    daysRemaining: 518,
+    san: ['*.rocketmq-prod.svc.cluster.local'],
+  },
+];
+
+export interface ClusterConfig {
+  writeQueueNums: number;
+  readQueueNums: number;
+  maxMessageSize: number;
+  msgTraceTopicName: string;
+  autoCreateTopicEnable: boolean;
+  autoCreateSubscriptionGroup: boolean;
+  deleteWhen: string;
+  fileReservedTime: number;
+  flushDiskType: 'ASYNC_FLUSH' | 'SYNC_FLUSH';
+  brokerPermission: number;
+}
+
+export interface ClusterInfo {
+  id: string;
+  name: string;
+  nsClusterName: string;
+  type: 'V5_PROXY_CLUSTER';
+  endpoint: string;
+  status: 'healthy' | 'warning' | 'error' | 'offline';
+  version: string;
+  brokers: BrokerInfo[];
+  proxies: ProxyInfo[];
+  nameServers: NameServerInfo[];
+  config: ClusterConfig;
+  topicCount: number;
+  groupCount: number;
+  tpsHistory: number[];
+}
+
+const clusters: ClusterInfo[] = [
+  {
+    id: 'cluster-prod',
+    name: 'rocketmq-prod',
+    nsClusterName: 'ns-prod',
+    type: 'V5_PROXY_CLUSTER',
+    endpoint: '10.101.2.1:9876',
+    status: 'healthy',
+    version: '5.2.0',
+    brokers: [
+      {
+        name: 'rocketmq-prod-0',
+        addr: '10.101.2.11:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 62,
+        tpsIn: 12480,
+        tpsOut: 34560,
+      },
+      {
+        name: 'rocketmq-prod-1',
+        addr: '10.101.2.12:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 58,
+        tpsIn: 11800,
+        tpsOut: 31200,
+      },
+      {
+        name: 'rocketmq-prod-2',
+        addr: '10.101.2.13:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 71,
+        tpsIn: 11200,
+        tpsOut: 28900,
+      },
+      {
+        name: 'rocketmq-prod-3',
+        addr: '10.101.2.14:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 55,
+        tpsIn: 10500,
+        tpsOut: 27800,
+      },
+      {
+        name: 'rocketmq-prod-4',
+        addr: '10.101.2.15:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 48,
+        tpsIn: 9800,
+        tpsOut: 25600,
+      },
+      {
+        name: 'rocketmq-prod-5',
+        addr: '10.101.2.16:10911',
+        version: '5.2.0',
+        status: 'readonly',
+        diskUsage: 86,
+        tpsIn: 0,
+        tpsOut: 18400,
+      },
+      {
+        name: 'rocketmq-prod-6',
+        addr: '10.101.2.17:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 43,
+        tpsIn: 8600,
+        tpsOut: 22400,
+      },
+      {
+        name: 'rocketmq-prod-7',
+        addr: '10.101.2.18:10911',
+        version: '5.1.4',
+        status: 'maintenance',
+        diskUsage: 91,
+        tpsIn: 0,
+        tpsOut: 0,
+      },
+    ],
+    proxies: [
+      {
+        addr: '10.101.2.21:8081',
+        status: 'healthy',
+        connections: 1842,
+        grpcPort: 8081,
+        remotingPort: 8080,
+      },
+      {
+        addr: '10.101.2.22:8081',
+        status: 'healthy',
+        connections: 1653,
+        grpcPort: 8081,
+        remotingPort: 8080,
+      },
+      {
+        addr: '10.101.2.23:8081',
+        status: 'healthy',
+        connections: 1724,
+        grpcPort: 8081,
+        remotingPort: 8080,
+      },
+    ],
+    nameServers: [
+      { addr: '10.101.2.1:9876', status: 'healthy' },
+      { addr: '10.101.2.2:9876', status: 'healthy' },
+      { addr: '10.101.2.3:9876', status: 'healthy' },
+    ],
+    config: {
+      writeQueueNums: 16,
+      readQueueNums: 16,
+      maxMessageSize: 4194304,
+      msgTraceTopicName: 'RMQ_SYS_TRACE_TOPIC4',
+      autoCreateTopicEnable: false,
+      autoCreateSubscriptionGroup: false,
+      deleteWhen: '04',
+      fileReservedTime: 72,
+      flushDiskType: 'SYNC_FLUSH',
+      brokerPermission: 6,
+    },
+    topicCount: 256,
+    groupCount: 128,
+    tpsHistory: [
+      18200, 19800, 21400, 20100, 23680, 24100, 22500, 25800, 24600, 23200, 22800, 24500,
+    ],
+  },
+  {
+    id: 'cluster-pre',
+    name: 'rocketmq-pre',
+    nsClusterName: 'ns-pre',
+    type: 'V5_PROXY_CLUSTER',
+    endpoint: '10.102.5.1:9876',
+    status: 'healthy',
+    version: '5.2.0',
+    brokers: [
+      {
+        name: 'rocketmq-pre-0',
+        addr: '10.102.5.11:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 35,
+        tpsIn: 2400,
+        tpsOut: 6800,
+      },
+      {
+        name: 'rocketmq-pre-1',
+        addr: '10.102.5.12:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 28,
+        tpsIn: 2100,
+        tpsOut: 5600,
+      },
+      {
+        name: 'rocketmq-pre-2',
+        addr: '10.102.5.13:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 42,
+        tpsIn: 1800,
+        tpsOut: 4800,
+      },
+      {
+        name: 'rocketmq-pre-3',
+        addr: '10.102.5.14:10911',
+        version: '5.2.0',
+        status: 'running',
+        diskUsage: 31,
+        tpsIn: 1500,
+        tpsOut: 4200,
+      },
+    ],
+    proxies: [
+      {
+        addr: '10.102.5.21:8081',
+        status: 'healthy',
+        connections: 424,
+        grpcPort: 8081,
+        remotingPort: 8080,
+      },
+      {
+        addr: '10.102.5.22:8081',
+        status: 'healthy',
+        connections: 382,
+        grpcPort: 8081,
+        remotingPort: 8080,
+      },
+    ],
+    nameServers: [
+      { addr: '10.102.5.1:9876', status: 'healthy' },
+      { addr: '10.102.5.2:9876', status: 'healthy' },
+    ],
+    config: {
+      writeQueueNums: 8,
+      readQueueNums: 8,
+      maxMessageSize: 4194304,
+      msgTraceTopicName: 'RMQ_SYS_TRACE_TOPIC4',
+      autoCreateTopicEnable: true,
+      autoCreateSubscriptionGroup: true,
+      deleteWhen: '04',
+      fileReservedTime: 24,
+      flushDiskType: 'ASYNC_FLUSH',
+      brokerPermission: 6,
+    },
+    topicCount: 48,
+    groupCount: 24,
+    tpsHistory: [5200, 5800, 6400, 6000, 7200, 6800, 6200, 7000, 7400, 6800, 7200, 7600],
+  },
+];
+
+export default clusters;
diff --git a/web/src/mock/consumers.ts b/web/src/mock/consumers.ts
new file mode 100644
index 0000000..0d80508
--- /dev/null
+++ b/web/src/mock/consumers.ts
@@ -0,0 +1,1070 @@
+/*
+ * 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.
+ */
+
+/* ─── Types ─── */
+
+export interface ConsumerInstance {
+  clientId: string;
+  protocol: 'REMOTING' | 'GRPC';
+  address: string;
+  subscribedTopics: string[];
+  lastHeartbeat: string;
+  topicLag: Record<string, number>;
+}
+
+export interface ConsumerGroup {
+  name: string;
+  namespace: string;
+  clusterId: string;
+  subscriptionMode: 'Push' | 'Pop';
+  consumeType: 'CLUSTERING' | 'BROADCASTING';
+  onlineInstances: number;
+  totalLag: number;
+  subscribedTopics: string[];
+  subscriptionDataType: 'NORMAL' | 'FIFO' | 'DELAY' | 'TRANSACTION';
+  deliveryOrderType?: 'PARTITON_ORDER' | 'MESSAGES_ORDER';
+  retryMaxTimes: number;
+  createdAt: string;
+  updatedAt: string;
+  delaySeconds: number;
+  instances: ConsumerInstance[];
+}
+
+export interface QueueProgress {
+  broker: string;
+  queueId: number;
+  brokerOffset: number;
+  consumerOffset: number;
+  diffTotal: number;
+}
+
+export interface SubscriptionEntry {
+  topic: string;
+  expression: string;
+  type: 'NORMAL' | 'FIFO' | 'DELAY' | 'TRANSACTION';
+  filterMode: 'Tag 过滤' | 'SQL92 过滤' | '全量';
+  consistency: '一致' | '不一致';
+}
+
+/* ─── Mock Data ─── */
+
+export const mockConsumerGroups: ConsumerGroup[] = [
+  {
+    name: 'cg-order-notify',
+    namespace: 'trade',
+    clusterId: 'hz-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 6,
+    totalLag: 2340,
+    subscribedTopics: ['order-create', 'order-status-change', 'payment-callback'],
+    subscriptionDataType: 'NORMAL',
+    retryMaxTimes: 16,
+    createdAt: '2026-03-15 10:30:00',
+    updatedAt: '2026-06-28 15:20:00',
+    delaySeconds: 245,
+    instances: [
+      {
+        clientId: 'order-notify-0@10.0.1.12',
+        protocol: 'GRPC',
+        address: '10.0.1.12:49152',
+        subscribedTopics: ['order-create', 'order-status-change'],
+        lastHeartbeat: '2026-07-01 18:32:05',
+        topicLag: { 'order-create': 180, 'order-status-change': 95 },
+      },
+      {
+        clientId: 'order-notify-1@10.0.1.13',
+        protocol: 'GRPC',
+        address: '10.0.1.13:49200',
+        subscribedTopics: ['order-create', 'payment-callback'],
+        lastHeartbeat: '2026-07-01 18:32:03',
+        topicLag: { 'order-create': 210, 'payment-callback': 88 },
+      },
+      {
+        clientId: 'order-notify-2@10.0.1.14',
+        protocol: 'GRPC',
+        address: '10.0.1.14:49300',
+        subscribedTopics: ['order-status-change', 'payment-callback'],
+        lastHeartbeat: '2026-07-01 18:32:01',
+        topicLag: { 'order-status-change': 156, 'payment-callback': 42 },
+      },
+      {
+        clientId: 'order-notify-3@10.0.1.15',
+        protocol: 'REMOTING',
+        address: '10.0.1.15:49400',
+        subscribedTopics: ['order-create', 'order-status-change', 'payment-callback'],
+        lastHeartbeat: '2026-07-01 18:31:58',
+        topicLag: { 'order-create': 320, 'order-status-change': 201, 'payment-callback': 67 },
+      },
+      {
+        clientId: 'order-notify-4@10.0.1.16',
+        protocol: 'GRPC',
+        address: '10.0.1.16:49500',
+        subscribedTopics: ['order-create'],
+        lastHeartbeat: '2026-07-01 18:32:04',
+        topicLag: { 'order-create': 455 },
+      },
+      {
+        clientId: 'order-notify-5@10.0.1.17',
+        protocol: 'GRPC',
+        address: '10.0.1.17:49600',
+        subscribedTopics: ['order-status-change', 'payment-callback'],
+        lastHeartbeat: '2026-07-01 18:32:02',
+        topicLag: { 'order-status-change': 312, 'payment-callback': 214 },
+      },
+    ],
+  },
+  {
+    name: 'cg-payment-callback',
+    namespace: 'trade',
+    clusterId: 'hz-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 4,
+    totalLag: 128,
+    subscribedTopics: ['payment-callback', 'refund-event'],
+    subscriptionDataType: 'TRANSACTION',
+    retryMaxTimes: 8,
+    createdAt: '2026-02-20 14:15:00',
+    updatedAt: '2026-06-15 10:45:00',
+    delaySeconds: 50,
+    instances: [
+      {
+        clientId: 'payment-cb-0@10.0.2.10',
+        protocol: 'GRPC',
+        address: '10.0.2.10:50100',
+        subscribedTopics: ['payment-callback', 'refund-event'],
+        lastHeartbeat: '2026-07-01 18:32:00',
+        topicLag: { 'payment-callback': 35, 'refund-event': 12 },
+      },
+      {
+        clientId: 'payment-cb-1@10.0.2.11',
+        protocol: 'GRPC',
+        address: '10.0.2.11:50200',
+        subscribedTopics: ['payment-callback'],
+        lastHeartbeat: '2026-07-01 18:31:58',
+        topicLag: { 'payment-callback': 28 },
+      },
+      {
+        clientId: 'payment-cb-2@10.0.2.12',
+        protocol: 'GRPC',
+        address: '10.0.2.12:50300',
+        subscribedTopics: ['refund-event'],
+        lastHeartbeat: '2026-07-01 18:32:02',
+        topicLag: { 'refund-event': 22 },
+      },
+      {
+        clientId: 'payment-cb-3@10.0.2.13',
+        protocol: 'REMOTING',
+        address: '10.0.2.13:50400',
+        subscribedTopics: ['payment-callback', 'refund-event'],
+        lastHeartbeat: '2026-07-01 18:31:55',
+        topicLag: { 'payment-callback': 18, 'refund-event': 13 },
+      },
+    ],
+  },
+  {
+    name: 'cg-user-activity',
+    namespace: 'user',
+    clusterId: 'hz-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'BROADCASTING',
+    onlineInstances: 3,
+    totalLag: 56700,
+    subscribedTopics: ['user-activity-log', 'user-profile-change'],
+    subscriptionDataType: 'NORMAL',
+    retryMaxTimes: 16,
+    createdAt: '2026-01-10 09:00:00',
+    updatedAt: '2026-05-20 16:30:00',
+    delaySeconds: 3725,
+    instances: [
+      {
+        clientId: 'user-act-0@10.0.3.20',
+        protocol: 'GRPC',
+        address: '10.0.3.20:51100',
+        subscribedTopics: ['user-activity-log', 'user-profile-change'],
+        lastHeartbeat: '2026-07-01 18:31:50',
+        topicLag: { 'user-activity-log': 18500, 'user-profile-change': 420 },
+      },
+      {
+        clientId: 'user-act-1@10.0.3.21',
+        protocol: 'GRPC',
+        address: '10.0.3.21:51200',
+        subscribedTopics: ['user-activity-log'],
+        lastHeartbeat: '2026-07-01 18:31:48',
+        topicLag: { 'user-activity-log': 19200 },
+      },
+      {
+        clientId: 'user-act-2@10.0.3.22',
+        protocol: 'REMOTING',
+        address: '10.0.3.22:51300',
+        subscribedTopics: ['user-activity-log', 'user-profile-change'],
+        lastHeartbeat: '2026-07-01 18:31:52',
+        topicLag: { 'user-activity-log': 17800, 'user-profile-change': 780 },
+      },
+    ],
+  },
+  {
+    name: 'cg-inventory-sync',
+    namespace: 'supply',
+    clusterId: 'sh-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 2,
+    totalLag: 890,
+    subscribedTopics: ['inventory-sync', 'stock-alert'],
+    subscriptionDataType: 'FIFO',
+    deliveryOrderType: 'PARTITON_ORDER',
+    retryMaxTimes: 3,
+    createdAt: '2026-04-05 11:20:00',
+    updatedAt: '2026-06-30 08:10:00',
+    delaySeconds: 720,
+    instances: [
+      {
+        clientId: 'inv-sync-0@10.0.4.30',
+        protocol: 'GRPC',
+        address: '10.0.4.30:52100',
+        subscribedTopics: ['inventory-sync', 'stock-alert'],
+        lastHeartbeat: '2026-07-01 18:31:45',
+        topicLag: { 'inventory-sync': 520, 'stock-alert': 85 },
+      },
+      {
+        clientId: 'inv-sync-1@10.0.4.31',
+        protocol: 'GRPC',
+        address: '10.0.4.31:52200',
+        subscribedTopics: ['inventory-sync'],
+        lastHeartbeat: '2026-07-01 18:31:42',
+        topicLag: { 'inventory-sync': 285 },
+      },
+    ],
+  },
+  {
+    name: 'cg-log-collector',
+    namespace: 'infra',
+    clusterId: 'hz-prod',
+    subscriptionMode: 'Pop',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 8,
+    totalLag: 12400,
+    subscribedTopics: ['app-log', 'access-log', 'error-log'],
+    subscriptionDataType: 'NORMAL',
+    retryMaxTimes: 5,
+    createdAt: '2026-05-12 16:00:00',
+    updatedAt: '2026-07-01 12:00:00',
+    delaySeconds: 1850,
+    instances: [
+      {
+        clientId: 'log-col-0@10.0.5.40',
+        protocol: 'GRPC',
+        address: '10.0.5.40:53100',
+        subscribedTopics: ['app-log', 'access-log'],
+        lastHeartbeat: '2026-07-01 18:32:08',
+        topicLag: { 'app-log': 1850, 'access-log': 620 },
+      },
+      {
+        clientId: 'log-col-1@10.0.5.41',
+        protocol: 'GRPC',
+        address: '10.0.5.41:53200',
+        subscribedTopics: ['app-log', 'error-log'],
+        lastHeartbeat: '2026-07-01 18:32:06',
+        topicLag: { 'app-log': 1720, 'error-log': 310 },
+      },
+      {
+        clientId: 'log-col-2@10.0.5.42',
+        protocol: 'GRPC',
+        address: '10.0.5.42:53300',
+        subscribedTopics: ['access-log', 'error-log'],
+        lastHeartbeat: '2026-07-01 18:32:04',
+        topicLag: { 'access-log': 1450, 'error-log': 280 },
+      },
+      {
+        clientId: 'log-col-3@10.0.5.43',
+        protocol: 'GRPC',
+        address: '10.0.5.43:53400',
+        subscribedTopics: ['app-log'],
+        lastHeartbeat: '2026-07-01 18:32:02',
+        topicLag: { 'app-log': 1980 },
+      },
+      {
+        clientId: 'log-col-4@10.0.5.44',
+        protocol: 'GRPC',
+        address: '10.0.5.44:53500',
+        subscribedTopics: ['access-log'],
+        lastHeartbeat: '2026-07-01 18:32:00',
+        topicLag: { 'access-log': 1100 },
+      },
+      {
+        clientId: 'log-col-5@10.0.5.45',
+        protocol: 'GRPC',
+        address: '10.0.5.45:53600',
+        subscribedTopics: ['error-log'],
+        lastHeartbeat: '2026-07-01 18:31:58',
+        topicLag: { 'error-log': 420 },
+      },
+      {
+        clientId: 'log-col-6@10.0.5.46',
+        protocol: 'REMOTING',
+        address: '10.0.5.46:53700',
+        subscribedTopics: ['app-log', 'access-log', 'error-log'],
+        lastHeartbeat: '2026-07-01 18:31:55',
+        topicLag: { 'app-log': 950, 'access-log': 580, 'error-log': 190 },
+      },
+      {
+        clientId: 'log-col-7@10.0.5.47',
+        protocol: 'GRPC',
+        address: '10.0.5.47:53800',
+        subscribedTopics: ['app-log', 'error-log'],
+        lastHeartbeat: '2026-07-01 18:31:52',
+        topicLag: { 'app-log': 820, 'error-log': 130 },
+      },
+    ],
+  },
+  {
+    name: 'cg-notification-push',
+    namespace: 'message',
+    clusterId: 'hz-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 3,
+    totalLag: 45,
+    subscribedTopics: ['notification-push', 'sms-gateway'],
+    subscriptionDataType: 'DELAY',
+    retryMaxTimes: 8,
+    createdAt: '2026-03-28 08:45:00',
+    updatedAt: '2026-06-22 14:30:00',
+    delaySeconds: 8,
+    instances: [
+      {
+        clientId: 'notif-push-0@10.0.6.50',
+        protocol: 'GRPC',
+        address: '10.0.6.50:54100',
+        subscribedTopics: ['notification-push'],
+        lastHeartbeat: '2026-07-01 18:32:10',
+        topicLag: { 'notification-push': 12 },
+      },
+      {
+        clientId: 'notif-push-1@10.0.6.51',
+        protocol: 'GRPC',
+        address: '10.0.6.51:54200',
+        subscribedTopics: ['notification-push', 'sms-gateway'],
+        lastHeartbeat: '2026-07-01 18:32:08',
+        topicLag: { 'notification-push': 8, 'sms-gateway': 15 },
+      },
+      {
+        clientId: 'notif-push-2@10.0.6.52',
+        protocol: 'GRPC',
+        address: '10.0.6.52:54300',
+        subscribedTopics: ['sms-gateway'],
+        lastHeartbeat: '2026-07-01 18:32:06',
+        topicLag: { 'sms-gateway': 10 },
+      },
+    ],
+  },
+  {
+    name: 'cg-ai-task-worker',
+    namespace: 'ai',
+    clusterId: 'hz-prod',
+    subscriptionMode: 'Pop',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 4,
+    totalLag: 3200,
+    subscribedTopics: ['ai-task-dispatch', 'model-inference-request'],
+    subscriptionDataType: 'NORMAL',
+    retryMaxTimes: 3,
+    createdAt: '2026-06-01 13:30:00',
+    updatedAt: '2026-07-01 09:15:00',
+    delaySeconds: 4800,
+    instances: [
+      {
+        clientId: 'ai-worker-0@10.0.7.60',
+        protocol: 'GRPC',
+        address: '10.0.7.60:55100',
+        subscribedTopics: ['ai-task-dispatch', 'model-inference-request'],
+        lastHeartbeat: '2026-07-01 18:32:12',
+        topicLag: { 'ai-task-dispatch': 450, 'model-inference-request': 380 },
+      },
+      {
+        clientId: 'ai-worker-1@10.0.7.61',
+        protocol: 'GRPC',
+        address: '10.0.7.61:55200',
+        subscribedTopics: ['ai-task-dispatch'],
+        lastHeartbeat: '2026-07-01 18:32:10',
+        topicLag: { 'ai-task-dispatch': 620 },
+      },
+      {
+        clientId: 'ai-worker-2@10.0.7.62',
+        protocol: 'GRPC',
+        address: '10.0.7.62:55300',
+        subscribedTopics: ['model-inference-request'],
+        lastHeartbeat: '2026-07-01 18:32:08',
+        topicLag: { 'model-inference-request': 850 },
+      },
+      {
+        clientId: 'ai-worker-3@10.0.7.63',
+        protocol: 'GRPC',
+        address: '10.0.7.63:55400',
+        subscribedTopics: ['ai-task-dispatch', 'model-inference-request'],
+        lastHeartbeat: '2026-07-01 18:32:06',
+        topicLag: { 'ai-task-dispatch': 380, 'model-inference-request': 520 },
+      },
+    ],
+  },
+  {
+    name: 'cg-metrics-aggregator',
+    namespace: 'infra',
+    clusterId: 'sh-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 2,
+    totalLag: 15800,
+    subscribedTopics: ['metrics-raw', 'trace-span'],
+    subscriptionDataType: 'NORMAL',
+    retryMaxTimes: 16,
+    createdAt: '2026-02-08 07:00:00',
+    updatedAt: '2026-06-10 11:45:00',
+    delaySeconds: 82500,
+    instances: [
+      {
+        clientId: 'metrics-agg-0@10.0.8.70',
+        protocol: 'GRPC',
+        address: '10.0.8.70:56100',
+        subscribedTopics: ['metrics-raw', 'trace-span'],
+        lastHeartbeat: '2026-07-01 18:31:40',
+        topicLag: { 'metrics-raw': 8500, 'trace-span': 3200 },
+      },
+      {
+        clientId: 'metrics-agg-1@10.0.8.71',
+        protocol: 'REMOTING',
+        address: '10.0.8.71:56200',
+        subscribedTopics: ['metrics-raw', 'trace-span'],
+        lastHeartbeat: '2026-07-01 18:31:38',
+        topicLag: { 'metrics-raw': 2800, 'trace-span': 1300 },
+      },
+    ],
+  },
+  {
+    name: 'cg-risk-control',
+    namespace: 'risk',
+    clusterId: 'hz-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 5,
+    totalLag: 4560,
+    subscribedTopics: ['transaction-event', 'login-event', 'payment-callback'],
+    subscriptionDataType: 'NORMAL',
+    retryMaxTimes: 8,
+    createdAt: '2025-11-20 10:00:00',
+    updatedAt: '2026-06-05 17:20:00',
+    delaySeconds: 1520,
+    instances: [
+      {
+        clientId: 'risk-ctrl-0@10.0.9.80',
+        protocol: 'GRPC',
+        address: '10.0.9.80:57100',
+        subscribedTopics: ['transaction-event', 'login-event'],
+        lastHeartbeat: '2026-07-01 18:32:15',
+        topicLag: { 'transaction-event': 520, 'login-event': 380 },
+      },
+      {
+        clientId: 'risk-ctrl-1@10.0.9.81',
+        protocol: 'GRPC',
+        address: '10.0.9.81:57200',
+        subscribedTopics: ['transaction-event', 'payment-callback'],
+        lastHeartbeat: '2026-07-01 18:32:13',
+        topicLag: { 'transaction-event': 480, 'payment-callback': 290 },
+      },
+      {
+        clientId: 'risk-ctrl-2@10.0.9.82',
+        protocol: 'GRPC',
+        address: '10.0.9.82:57300',
+        subscribedTopics: ['login-event', 'payment-callback'],
+        lastHeartbeat: '2026-07-01 18:32:11',
+        topicLag: { 'login-event': 410, 'payment-callback': 350 },
+      },
+      {
+        clientId: 'risk-ctrl-3@10.0.9.83',
+        protocol: 'GRPC',
+        address: '10.0.9.83:57400',
+        subscribedTopics: ['transaction-event', 'login-event', 'payment-callback'],
+        lastHeartbeat: '2026-07-01 18:32:09',
+        topicLag: { 'transaction-event': 600, 'login-event': 420, 'payment-callback': 310 },
+      },
+      {
+        clientId: 'risk-ctrl-4@10.0.9.84',
+        protocol: 'REMOTING',
+        address: '10.0.9.84:57500',
+        subscribedTopics: ['transaction-event'],
+        lastHeartbeat: '2026-07-01 18:32:07',
+        topicLag: { 'transaction-event': 800 },
+      },
+    ],
+  },
+  {
+    name: 'cg-data-sync',
+    namespace: 'data',
+    clusterId: 'sh-prod',
+    subscriptionMode: 'Push',
+    consumeType: 'CLUSTERING',
+    onlineInstances: 3,
+    totalLag: 18700,
+    subscribedTopics: ['binlog-event', 'schema-change'],
+    subscriptionDataType: 'FIFO',
+    deliveryOrderType: 'PARTITON_ORDER',
+    retryMaxTimes: 5,
+    createdAt: '2025-09-15 08:30:00',
+    updatedAt: '2026-06-28 20:00:00',
+    delaySeconds: 12600,
+    instances: [
+      {
+        clientId: 'data-sync-0@10.0.10.90',
+        protocol: 'GRPC',
+        address: '10.0.10.90:58100',
+        subscribedTopics: ['binlog-event', 'schema-change'],
+        lastHeartbeat: '2026-07-01 18:31:55',
+        topicLag: { 'binlog-event': 8500, 'schema-change': 120 },
+      },
+      {
+        clientId: 'data-sync-1@10.0.10.91',
+        protocol: 'GRPC',
+        address: '10.0.10.91:58200',
+        subscribedTopics: ['binlog-event'],
+        lastHeartbeat: '2026-07-01 18:31:53',
+        topicLag: { 'binlog-event': 9200 },
+      },
+      {
+        clientId: 'data-sync-2@10.0.10.92',
+        protocol: 'GRPC',
+        address: '10.0.10.92:58300',
+        subscribedTopics: ['binlog-event', 'schema-change'],
+        lastHeartbeat: '2026-07-01 18:31:50',
+        topicLag: { 'binlog-event': 7800, 'schema-change': 80 },
+      },
+    ],
+  },
+];
+
+/* ─── Queue Progress (per group) ─── */
+
+export const mockQueueProgress: Record<string, QueueProgress[]> = {
+  'cg-order-notify': [
+    {
+      broker: 'broker-hz-0',
+      queueId: 0,
+      brokerOffset: 1_580_420,
+      consumerOffset: 1_579_850,
+      diffTotal: 570,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 1,
+      brokerOffset: 1_579_200,
+      consumerOffset: 1_578_900,
+      diffTotal: 300,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 2,
+      brokerOffset: 1_578_800,
+      consumerOffset: 1_578_380,
+      diffTotal: 420,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 0,
+      brokerOffset: 1_420_300,
+      consumerOffset: 1_419_950,
+      diffTotal: 350,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 1,
+      brokerOffset: 1_419_800,
+      consumerOffset: 1_419_450,
+      diffTotal: 350,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 2,
+      brokerOffset: 1_418_500,
+      consumerOffset: 1_418_150,
+      diffTotal: 350,
+    },
+  ],
+  'cg-payment-callback': [
+    {
+      broker: 'broker-hz-0',
+      queueId: 0,
+      brokerOffset: 892_100,
+      consumerOffset: 892_068,
+      diffTotal: 32,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 1,
+      brokerOffset: 891_500,
+      consumerOffset: 891_468,
+      diffTotal: 32,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 0,
+      brokerOffset: 780_200,
+      consumerOffset: 780_168,
+      diffTotal: 32,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 1,
+      brokerOffset: 779_800,
+      consumerOffset: 779_768,
+      diffTotal: 32,
+    },
+  ],
+  'cg-user-activity': [
+    {
+      broker: 'broker-hz-0',
+      queueId: 0,
+      brokerOffset: 5_680_000,
+      consumerOffset: 5_661_200,
+      diffTotal: 18800,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 1,
+      brokerOffset: 5_675_000,
+      consumerOffset: 5_655_500,
+      diffTotal: 19500,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 0,
+      brokerOffset: 4_920_000,
+      consumerOffset: 4_910_700,
+      diffTotal: 9300,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 1,
+      brokerOffset: 4_915_000,
+      consumerOffset: 4_905_900,
+      diffTotal: 9100,
+    },
+  ],
+  'cg-inventory-sync': [
+    {
+      broker: 'broker-sh-0',
+      queueId: 0,
+      brokerOffset: 320_800,
+      consumerOffset: 320_355,
+      diffTotal: 445,
+    },
+    {
+      broker: 'broker-sh-0',
+      queueId: 1,
+      brokerOffset: 320_100,
+      consumerOffset: 319_655,
+      diffTotal: 445,
+    },
+  ],
+  'cg-log-collector': [
+    {
+      broker: 'broker-hz-0',
+      queueId: 0,
+      brokerOffset: 12_800_000,
+      consumerOffset: 12_798_450,
+      diffTotal: 1550,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 1,
+      brokerOffset: 12_790_000,
+      consumerOffset: 12_788_300,
+      diffTotal: 1700,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 2,
+      brokerOffset: 12_780_000,
+      consumerOffset: 12_778_600,
+      diffTotal: 1400,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 3,
+      brokerOffset: 12_770_000,
+      consumerOffset: 12_768_700,
+      diffTotal: 1300,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 0,
+      brokerOffset: 11_200_000,
+      consumerOffset: 11_198_450,
+      diffTotal: 1550,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 1,
+      brokerOffset: 11_190_000,
+      consumerOffset: 11_188_450,
+      diffTotal: 1550,
+    },
+    {
+      broker: 'broker-hz-2',
+      queueId: 0,
+      brokerOffset: 9_800_000,
+      consumerOffset: 9_798_200,
+      diffTotal: 1800,
+    },
+    {
+      broker: 'broker-hz-2',
+      queueId: 1,
+      brokerOffset: 9_790_000,
+      consumerOffset: 9_788_450,
+      diffTotal: 1550,
+    },
+  ],
+  'cg-notification-push': [
+    {
+      broker: 'broker-hz-0',
+      queueId: 0,
+      brokerOffset: 450_200,
+      consumerOffset: 450_185,
+      diffTotal: 15,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 1,
+      brokerOffset: 449_800,
+      consumerOffset: 449_785,
+      diffTotal: 15,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 0,
+      brokerOffset: 380_100,
+      consumerOffset: 380_092,
+      diffTotal: 8,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 1,
+      brokerOffset: 379_800,
+      consumerOffset: 379_793,
+      diffTotal: 7,
+    },
+  ],
+  'cg-ai-task-worker': [
+    {
+      broker: 'broker-hz-0',
+      queueId: 0,
+      brokerOffset: 240_800,
+      consumerOffset: 240_000,
+      diffTotal: 800,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 1,
+      brokerOffset: 240_200,
+      consumerOffset: 239_400,
+      diffTotal: 800,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 0,
+      brokerOffset: 210_600,
+      consumerOffset: 209_800,
+      diffTotal: 800,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 1,
+      brokerOffset: 210_000,
+      consumerOffset: 209_200,
+      diffTotal: 800,
+    },
+  ],
+  'cg-metrics-aggregator': [
+    {
+      broker: 'broker-sh-0',
+      queueId: 0,
+      brokerOffset: 8_900_000,
+      consumerOffset: 8_892_100,
+      diffTotal: 7900,
+    },
+    {
+      broker: 'broker-sh-0',
+      queueId: 1,
+      brokerOffset: 8_890_000,
+      consumerOffset: 8_882_100,
+      diffTotal: 7900,
+    },
+  ],
+  'cg-risk-control': [
+    {
+      broker: 'broker-hz-0',
+      queueId: 0,
+      brokerOffset: 3_200_000,
+      consumerOffset: 3_199_100,
+      diffTotal: 900,
+    },
+    {
+      broker: 'broker-hz-0',
+      queueId: 1,
+      brokerOffset: 3_198_000,
+      consumerOffset: 3_197_200,
+      diffTotal: 800,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 0,
+      brokerOffset: 2_800_000,
+      consumerOffset: 2_799_060,
+      diffTotal: 940,
+    },
+    {
+      broker: 'broker-hz-1',
+      queueId: 1,
+      brokerOffset: 2_795_000,
+      consumerOffset: 2_794_080,
+      diffTotal: 920,
+    },
+    {
+      broker: 'broker-hz-2',
+      queueId: 0,
+      brokerOffset: 2_400_000,
+      consumerOffset: 2_399_000,
+      diffTotal: 1000,
+    },
+  ],
+  'cg-data-sync': [
+    {
+      broker: 'broker-sh-0',
+      queueId: 0,
+      brokerOffset: 6_500_000,
+      consumerOffset: 6_493_500,
+      diffTotal: 6500,
+    },
+    {
+      broker: 'broker-sh-0',
+      queueId: 1,
+      brokerOffset: 6_490_000,
+      consumerOffset: 6_483_800,
+      diffTotal: 6200,
+    },
+    {
+      broker: 'broker-sh-1',
+      queueId: 0,
+      brokerOffset: 5_800_000,
+      consumerOffset: 5_797_000,
+      diffTotal: 3000,
+    },
+    {
+      broker: 'broker-sh-1',
+      queueId: 1,
+      brokerOffset: 5_795_000,
+      consumerOffset: 5_792_000,
+      diffTotal: 3000,
+    },
+  ],
+};
+
+/* ─── Subscription Details (per group) ─── */
+
+export const mockSubscriptions: Record<string, SubscriptionEntry[]> = {
+  'cg-order-notify': [
+    {
+      topic: 'order-create',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+    {
+      topic: 'order-status-change',
+      expression: 'STATUS_PAID || STATUS_SHIPPED',
+      type: 'NORMAL',
+      filterMode: 'Tag 过滤',
+      consistency: '一致',
+    },
+    {
+      topic: 'payment-callback',
+      expression: '*',
+      type: 'TRANSACTION',
+      filterMode: '全量',
+      consistency: '不一致',
+    },
+  ],
+  'cg-payment-callback': [
+    {
+      topic: 'payment-callback',
+      expression: '*',
+      type: 'TRANSACTION',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+    {
+      topic: 'refund-event',
+      expression: 'AMOUNT > 100',
+      type: 'TRANSACTION',
+      filterMode: 'SQL92 过滤',
+      consistency: '一致',
+    },
+  ],
+  'cg-user-activity': [
+    {
+      topic: 'user-activity-log',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+    {
+      topic: 'user-profile-change',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+  ],
+  'cg-inventory-sync': [
+    {
+      topic: 'inventory-sync',
+      expression: '*',
+      type: 'FIFO',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+    {
+      topic: 'stock-alert',
+      expression: 'LEVEL < 10',
+      type: 'FIFO',
+      filterMode: 'SQL92 过滤',
+      consistency: '一致',
+    },
+  ],
+  'cg-log-collector': [
+    {
+      topic: 'app-log',
+      expression: 'LEVEL in (ERROR, WARN)',
+      type: 'NORMAL',
+      filterMode: 'SQL92 过滤',
+      consistency: '一致',
+    },
+    {
+      topic: 'access-log',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '不一致',
+    },
+    {
+      topic: 'error-log',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+  ],
+  'cg-notification-push': [
+    {
+      topic: 'notification-push',
+      expression: 'PRIORITY >= 3',
+      type: 'DELAY',
+      filterMode: 'SQL92 过滤',
+      consistency: '一致',
+    },
+    {
+      topic: 'sms-gateway',
+      expression: '*',
+      type: 'DELAY',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+  ],
+  'cg-ai-task-worker': [
+    {
+      topic: 'ai-task-dispatch',
+      expression: 'MODEL in (GPT, CLAUDE)',
+      type: 'NORMAL',
+      filterMode: 'SQL92 过滤',
+      consistency: '一致',
+    },
+    {
+      topic: 'model-inference-request',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+  ],
+  'cg-metrics-aggregator': [
+    {
+      topic: 'metrics-raw',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+    {
+      topic: 'trace-span',
+      expression: 'DURATION > 500',
+      type: 'NORMAL',
+      filterMode: 'SQL92 过滤',
+      consistency: '不一致',
+    },
+  ],
+  'cg-risk-control': [
+    {
+      topic: 'transaction-event',
+      expression: 'AMOUNT > 5000',
+      type: 'NORMAL',
+      filterMode: 'SQL92 过滤',
+      consistency: '一致',
+    },
+    {
+      topic: 'login-event',
+      expression: '*',
+      type: 'NORMAL',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+    {
+      topic: 'payment-callback',
+      expression: 'RISK_LEVEL >= 2',
+      type: 'NORMAL',
+      filterMode: 'SQL92 过滤',
+      consistency: '一致',
+    },
+  ],
+  'cg-data-sync': [
+    {
+      topic: 'binlog-event',
+      expression: '*',
+      type: 'FIFO',
+      filterMode: '全量',
+      consistency: '一致',
+    },
+    {
+      topic: 'schema-change',
+      expression: 'DDL',
+      type: 'FIFO',
+      filterMode: 'Tag 过滤',
+      consistency: '一致',
+    },
+  ],
+};
diff --git a/web/src/mock/dashboard.ts b/web/src/mock/dashboard.ts
new file mode 100644
index 0000000..64ba88a
--- /dev/null
+++ b/web/src/mock/dashboard.ts
@@ -0,0 +1,155 @@
+/*
+ * 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.
+ */
+
+export const dashboardStats = {
+  totalClusters: 2,
+  healthyClusters: 2,
+  totalBrokers: 12,
+  totalProxies: 5,
+  totalNameServers: 5,
+  totalTopics: 304,
+  totalConsumerGroups: 152,
+  totalMessagesToday: 18_420_000,
+  messagesPerSecond: 85_800,
+  tpsIn: 85_800,
+  tpsOut: 222_400,
+};
+
+export const clusterOverview = [
+  {
+    id: 'cluster-prod',
+    name: 'rocketmq-prod',
+    type: 'V5_PROXY_CLUSTER',
+    status: 'healthy' as const,
+    brokers: 8,
+    proxies: 3,
+    topics: 256,
+    groups: 128,
+    tpsIn: 78_000,
+    tpsOut: 203_600,
+    version: '5.2.0',
+    throughput: [
+      62000, 68000, 74000, 71000, 78000, 82000, 79000, 85000, 81000, 78000, 76000, 80000,
+    ],
+  },
+  {
+    id: 'cluster-pre',
+    name: 'rocketmq-pre',
+    type: 'V5_PROXY_CLUSTER',
+    status: 'healthy' as const,
+    brokers: 4,
+    proxies: 2,
+    topics: 48,
+    groups: 24,
+    tpsIn: 7_800,
+    tpsOut: 21_400,
+    version: '5.2.0',
+    throughput: [5200, 5800, 6400, 6000, 7200, 6800, 6200, 7000, 7400, 6800, 7200, 7600],
+  },
+];
+
+export const recentActivities = [
+  {
+    time: '18:32:05',
+    type: 'topic',
+    message: 'Topic order-payment 创建于 rocketmq-prod',
+    color: 'blue',
+  },
+  {
+    time: '18:28:17',
+    type: 'consumer',
+    message: '消费者组 cg-order-notify 重置消费位点至 1 小时前',
+    color: 'orange',
+  },
+  {
+    time: '18:15:42',
+    type: 'alert',
+    message: 'rocketmq-prod-5 磁盘使用率达到 86%,已切为只读',
+    color: 'red',
+  },
+  {
+    time: '17:58:30',
+    type: 'acl',
+    message: 'ACL 规则更新: user-order-service → Topic order-* [PUB, SUB]',
+    color: 'purple',
+  },
+  {
+    time: '17:42:11',
+    type: 'cluster',
+    message: 'rocketmq-prod-7 进入维护模式,禁止读写',
+    color: 'gold',
+  },
+  {
+    time: '17:30:00',
+    type: 'topic',
+    message: 'LiteTopic chat/sess-a3f8* 自动创建 128 个 (TTL: 24h)',
+    color: 'magenta',
+  },
+  {
+    time: '16:55:23',
+    type: 'consumer',
+    message: '消费者组 cg-payment-callback 新增 2 个在线实例',
+    color: 'cyan',
+  },
+  {
+    time: '16:20:45',
+    type: 'alert',
+    message: 'rocketmq-prod-7 磁盘使用率达到 91%,触发维护',
+    color: 'red',
+  },
+];
+
+export const topTopicsByThroughput = [
+  {
+    name: 'order-create',
+    namespace: 'trade',
+    type: 'TRANSACTION',
+    tps: 12480,
+    messages: 3_680_000,
+  },
+  { name: 'payment-notify', namespace: 'trade', type: 'NORMAL', tps: 11800, messages: 3_480_000 },
+  { name: 'user-login-event', namespace: 'user', type: 'NORMAL', tps: 10500, messages: 3_100_000 },
+  { name: 'inventory-sync', namespace: 'supply', type: 'FIFO', tps: 9800, messages: 2_890_000 },
+  { name: 'promo-push', namespace: 'marketing', type: 'DELAY', tps: 8600, messages: 2_540_000 },
+];
+
+export const systemAlerts = [
+  {
+    id: 'alert-1',
+    level: 'warning' as const,
+    title: 'rocketmq-prod-5 磁盘使用率偏高',
+    description: '磁盘使用率 86%,已自动切为只读模式',
+    time: '18:15',
+    acknowledged: false,
+  },
+  {
+    id: 'alert-2',
+    level: 'error' as const,
+    title: 'rocketmq-prod-7 进入维护模式',
+    description: '磁盘使用率 91%,已禁止读写,需人工介入处理',
+    time: '17:42',
+    acknowledged: false,
+  },
+  {
+    id: 'alert-3',
+    level: 'info' as const,
+    title: 'rocketmq-prod-7 版本落后',
+    description: '当前版本 5.1.4,集群版本 5.2.0,建议升级',
+    time: '16:30',
+    acknowledged: true,
+  },
+];
diff --git a/web/src/mock/dlq.ts b/web/src/mock/dlq.ts
new file mode 100644
index 0000000..c46a750
--- /dev/null
+++ b/web/src/mock/dlq.ts
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+export interface DLQGroup {
+  groupName: string;
+  dlqTopic: string;
+  messageCount: number;
+  lastEnqueueTime: string;
+  retryCount: number;
+  status: 'active' | 'empty';
+}
+
+export const mockDLQGroups: DLQGroup[] = [
+  {
+    groupName: 'cg-order-processor',
+    dlqTopic: '%DLQ%cg-order-processor',
+    messageCount: 23,
+    lastEnqueueTime: '2026-07-01T14:32:18.000Z',
+    retryCount: 16,
+    status: 'active',
+  },
+  {
+    groupName: 'cg-payment-handler',
+    dlqTopic: '%DLQ%cg-payment-handler',
+    messageCount: 8,
+    lastEnqueueTime: '2026-07-01T15:10:45.000Z',
+    retryCount: 16,
+    status: 'active',
+  },
+  {
+    groupName: 'cg-notification',
+    dlqTopic: '%DLQ%cg-notification',
+    messageCount: 156,
+    lastEnqueueTime: '2026-07-01T16:05:22.000Z',
+    retryCount: 16,
+    status: 'active',
+  },
+  {
+    groupName: 'cg-inventory-sync',
+    dlqTopic: '%DLQ%cg-inventory-sync',
+    messageCount: 3,
+    lastEnqueueTime: '2026-07-01T12:48:33.000Z',
+    retryCount: 16,
+    status: 'active',
+  },
+  {
+    groupName: 'cg-analytics',
+    dlqTopic: '%DLQ%cg-analytics',
+    messageCount: 42,
+    lastEnqueueTime: '2026-07-01T15:55:10.000Z',
+    retryCount: 16,
+    status: 'active',
+  },
+  {
+    groupName: 'cg-user-tracking',
+    dlqTopic: '%DLQ%cg-user-tracking',
+    messageCount: 0,
+    lastEnqueueTime: '2026-06-28T09:12:00.000Z',
+    retryCount: 16,
+    status: 'empty',
+  },
+  {
+    groupName: 'cg-sms-gateway',
+    dlqTopic: '%DLQ%cg-sms-gateway',
+    messageCount: 67,
+    lastEnqueueTime: '2026-07-01T16:20:55.000Z',
+    retryCount: 16,
+    status: 'active',
+  },
+  {
+    groupName: 'cg-email-service',
+    dlqTopic: '%DLQ%cg-email-service',
+    messageCount: 0,
+    lastEnqueueTime: '2026-06-30T18:05:12.000Z',
+    retryCount: 16,
+    status: 'empty',
+  },
+];
diff --git a/web/src/mock/instances.ts b/web/src/mock/instances.ts
new file mode 100644
index 0000000..bdf45f9
--- /dev/null
+++ b/web/src/mock/instances.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Instance } from '../api/instance';
+
+export const mockInstances: Instance[] = [
+  {
+    id: '1',
+    name: 'rocketmq-trade',
+    remark: '核心交易链路,承载订单、支付等主要业务',
+    type: 'PROXY',
+    endpoint: 'proxy-hz.rocketmq.internal:8080',
+    topicCount: 128,
+    consumerGroupCount: 56,
+    createdAt: '2024-03-15 08:30:00',
+    updatedAt: '2026-06-20 14:15:00',
+  },
+  {
+    id: '2',
+    name: 'rocketmq-dr',
+    remark: '灾备集群,与 trade 集群互为双活',
+    type: 'PROXY',
+    endpoint: 'proxy-sh.rocketmq.internal:8080',
+    topicCount: 96,
+    consumerGroupCount: 42,
+    createdAt: '2024-05-10 10:00:00',
+    updatedAt: '2026-06-18 09:30:00',
+  },
+  {
+    id: '3',
+    name: 'rocketmq-debug',
+    remark: '开发测试环境,仅供内部调试使用',
+    type: 'PROXY',
+    endpoint: 'localhost:8081',
+    topicCount: 15,
+    consumerGroupCount: 8,
+    createdAt: '2025-01-20 14:00:00',
+    updatedAt: '2026-07-01 11:45:00',
+  },
+  {
+    id: '4',
+    name: 'rocketmq-legacy',
+    remark: '旧版集群,计划 Q3 完成迁移后下线',
+    type: 'DIRECT',
+    endpoint: 'namesrv-legacy:9876',
+    topicCount: 64,
+    consumerGroupCount: 30,
+    createdAt: '2022-08-01 09:00:00',
+    updatedAt: '2025-12-10 16:20:00',
+  },
+  {
+    id: '5',
+    name: 'rocketmq-staging',
+    remark: '预发布验证环境,与生产配置一致',
+    type: 'PROXY',
+    endpoint: 'proxy-staging:8080',
+    topicCount: 32,
+    consumerGroupCount: 18,
+    createdAt: '2024-11-05 11:30:00',
+    updatedAt: '2026-06-25 08:00:00',
+  },
+];
diff --git a/web/src/mock/messages.ts b/web/src/mock/messages.ts
new file mode 100644
index 0000000..cd5351c
--- /dev/null
+++ b/web/src/mock/messages.ts
@@ -0,0 +1,1268 @@
+/*
+ * 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.
+ */
+
+export interface MessageRecord {
+  msgId: string;
+  topic: string;
+  tag: string;
+  key: string;
+  body: string;
+  storeTime: string;
+  bornHost: string;
+  storeHost: string;
+  properties: Record<string, string>;
+  size: number;
+}
+
+export interface TraceNode {
+  title: string;
+  timestamp: string;
+  status: 'finish' | 'process' | 'wait';
+  costTime: number;
+  description: string;
+}
+
+export interface ConsumerStatus {
+  group: string;
+  deliveryStatus: 'success' | 'failed' | 'pending';
+  consumeTime: string;
+  retryCount: number;
+}
+
+export interface TraceRecord {
+  nodes: TraceNode[];
+  consumerStatus: ConsumerStatus[];
+}
+
+export const mockMessages: MessageRecord[] = [
+  // ── order-create (5 messages) ──
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A3F2B1',
+    topic: 'order-create',
+    tag: 'create',
+    key: 'order-12345',
+    body: '{"orderId":"20240101001","userId":10001,"items":[{"skuId":"SKU001","quantity":2,"price":99.50}],"totalAmount":199.00}',
+    storeTime: '2026-07-01T10:23:45.123Z',
+    bornHost: '10.0.1.12:54321',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'order-12345',
+      TAGS: 'create',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A3F2B1',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 3617,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A3F7C2',
+    topic: 'order-create',
+    tag: 'create',
+    key: 'order-12346',
+    body: '{"orderId":"20240101002","userId":10042,"items":[{"skuId":"SKU015","quantity":1,"price":59.00}],"totalAmount":59.00}',
+    storeTime: '2026-07-01T10:25:12.456Z',
+    bornHost: '10.0.1.12:54322',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'order-12346',
+      TAGS: 'create',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A3F7C2',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 486,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A401D3',
+    topic: 'order-create',
+    tag: 'create',
+    key: 'order-12347',
+    body: '{"orderId":"20240101003","userId":10099,"items":[{"skuId":"SKU008","quantity":3,"price":33.00},{"skuId":"SKU012","quantity":1,"price":128.00}],"totalAmount":227.00}',
+    storeTime: '2026-07-01T10:30:05.789Z',
+    bornHost: '10.0.1.13:54321',
+    storeHost: '10.0.2.4:10911',
+    properties: {
+      KEYS: 'order-12347',
+      TAGS: 'create',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A401D3',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 1331200,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A40BE4',
+    topic: 'order-create',
+    tag: 'create',
+    key: 'order-12348',
+    body: '{"orderId":"20240101004","userId":10201,"items":[{"skuId":"SKU003","quantity":1,"price":1299.00}],"totalAmount":1299.00}',
+    storeTime: '2026-07-01T11:15:33.234Z',
+    bornHost: '10.0.1.12:54323',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'order-12348',
+      TAGS: 'create',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A40BE4',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 456,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A415F5',
+    topic: 'order-create',
+    tag: 'create',
+    key: 'order-12349',
+    body: '{"orderId":"20240101005","userId":10077,"items":[{"skuId":"SKU022","quantity":2,"price":45.00}],"totalAmount":90.00}',
+    storeTime: '2026-07-01T12:00:18.567Z',
+    bornHost: '10.0.1.14:54321',
+    storeHost: '10.0.2.5:10911',
+    properties: {
+      KEYS: 'order-12349',
+      TAGS: 'create',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A415F5',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 442,
+  },
+
+  // ── payment-callback (5 messages) ──
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A42006',
+    topic: 'payment-callback',
+    tag: 'success',
+    key: 'pay-67890',
+    body: '{"paymentId":"PAY20240101001","orderId":"20240101001","amount":199.00,"channel":"alipay","status":"PAID","paidAt":"2026-07-01T10:24:02.000Z"}',
+    storeTime: '2026-07-01T10:24:05.890Z',
+    bornHost: '10.0.1.20:54400',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'pay-67890',
+      TAGS: 'success',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A42006',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+      TRANSACTION_ID: 'TXN20240101001',
+    },
+    size: 8724,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A42A17',
+    topic: 'payment-callback',
+    tag: 'success',
+    key: 'pay-67891',
+    body: '{"paymentId":"PAY20240101002","orderId":"20240101002","amount":59.00,"channel":"wechat","status":"PAID","paidAt":"2026-07-01T10:26:15.000Z"}',
+    storeTime: '2026-07-01T10:26:18.123Z',
+    bornHost: '10.0.1.20:54401',
+    storeHost: '10.0.2.4:10911',
+    properties: {
+      KEYS: 'pay-67891',
+      TAGS: 'success',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A42A17',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 598,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A43428',
+    topic: 'payment-callback',
+    tag: 'failed',
+    key: 'pay-67892',
+    body: '{"paymentId":"PAY20240101003","orderId":"20240101003","amount":227.00,"channel":"alipay","status":"FAILED","reason":"insufficient_balance"}',
+    storeTime: '2026-07-01T10:32:45.456Z',
+    bornHost: '10.0.1.21:54400',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'pay-67892',
+      TAGS: 'failed',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A43428',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 672,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A43E39',
+    topic: 'payment-callback',
+    tag: 'success',
+    key: 'pay-67893',
+    body: '{"paymentId":"PAY20240101004","orderId":"20240101004","amount":1299.00,"channel":"bank_card","status":"PAID","paidAt":"2026-07-01T11:16:05.000Z"}',
+    storeTime: '2026-07-01T11:16:08.789Z',
+    bornHost: '10.0.1.20:54402',
+    storeHost: '10.0.2.5:10911',
+    properties: {
+      KEYS: 'pay-67893',
+      TAGS: 'success',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A43E39',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 2457600,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4484A',
+    topic: 'payment-callback',
+    tag: 'refunded',
+    key: 'pay-67894',
+    body: '{"paymentId":"PAY20240101005","orderId":"20240101005","amount":90.00,"channel":"wechat","status":"REFUNDED","refundAmount":90.00,"reason":"user_request"}',
+    storeTime: '2026-07-01T12:05:22.012Z',
+    bornHost: '10.0.1.21:54401',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'pay-67894',
+      TAGS: 'refunded',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4484A',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 708,
+  },
+
+  // ── user-activity-log (4 messages) ──
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4525B',
+    topic: 'user-activity-log',
+    tag: 'login',
+    key: 'user-10001',
+    body: '{"userId":10001,"action":"login","device":"iOS 17.2","ip":"223.104.3.21","timestamp":"2026-07-01T09:15:00.000Z"}',
+    storeTime: '2026-07-01T09:15:01.345Z',
+    bornHost: '10.0.1.30:55000',
+    storeHost: '10.0.2.4:10911',
+    properties: {
+      KEYS: 'user-10001',
+      TAGS: 'login',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4525B',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 5248,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A45C6C',
+    topic: 'user-activity-log',
+    tag: 'browse',
+    key: 'user-10042',
+    body: '{"userId":10042,"action":"browse","page":"product_detail","productId":"SKU015","duration":45}',
+    storeTime: '2026-07-01T09:30:12.678Z',
+    bornHost: '10.0.1.31:55001',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'user-10042',
+      TAGS: 'browse',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A45C6C',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 312,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4667D',
+    topic: 'user-activity-log',
+    tag: 'cart',
+    key: 'user-10099',
+    body: '{"userId":10099,"action":"add_to_cart","skuId":"SKU008","quantity":3,"cartTotal":5}',
+    storeTime: '2026-07-01T10:05:33.901Z',
+    bornHost: '10.0.1.30:55002',
+    storeHost: '10.0.2.5:10911',
+    properties: {
+      KEYS: 'user-10099',
+      TAGS: 'cart',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4667D',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 288,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4708E',
+    topic: 'user-activity-log',
+    tag: 'logout',
+    key: 'user-10001',
+    body: '{"userId":10001,"action":"logout","sessionDuration":3600}',
+    storeTime: '2026-07-01T10:15:45.234Z',
+    bornHost: '10.0.1.30:55000',
+    storeHost: '10.0.2.4:10911',
+    properties: {
+      KEYS: 'user-10001',
+      TAGS: 'logout',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4708E',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 214,
+  },
+
+  // ── notification-push (4 messages) ──
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A47A9F',
+    topic: 'notification-push',
+    tag: 'push',
+    key: 'notif-001',
+    body: '{"notificationId":"N001","userId":10001,"type":"order_status","title":"订单已发货","content":"您的订单 20240101001 已发货,快递单号 SF1234567890","channel":"app"}',
+    storeTime: '2026-07-01T13:00:05.567Z',
+    bornHost: '10.0.1.40:55100',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'notif-001',
+      TAGS: 'push',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A47A9F',
+      WAIT: 'true',
+      DELAY: '3',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 15872,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A484B0',
+    topic: 'notification-push',
+    tag: 'sms',
+    key: 'notif-002',
+    body: '{"notificationId":"N002","userId":10042,"type":"verification","phone":"138****5678","code":"892341","expireMinutes":5}',
+    storeTime: '2026-07-01T13:10:22.890Z',
+    bornHost: '10.0.1.41:55101',
+    storeHost: '10.0.2.4:10911',
+    properties: {
+      KEYS: 'notif-002',
+      TAGS: 'sms',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A484B0',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 428,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A48EC1',
+    topic: 'notification-push',
+    tag: 'email',
+    key: 'notif-003',
+    body: '{"notificationId":"N003","userId":10099,"type":"weekly_report","email":"user@example.com","subject":"本周消费报告","templateId":"TPL_WEEKLY"}',
+    storeTime: '2026-07-01T14:00:45.123Z',
+    bornHost: '10.0.1.40:55102',
+    storeHost: '10.0.2.5:10911',
+    properties: {
+      KEYS: 'notif-003',
+      TAGS: 'email',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A48EC1',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 512,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A498D2',
+    topic: 'notification-push',
+    tag: 'push',
+    key: 'notif-004',
+    body: '{"notificationId":"N004","userId":10201,"type":"promotion","title":"限时优惠","content":"您关注的商品 SKU003 降价 200 元,立即查看","channel":"app"}',
+    storeTime: '2026-07-01T15:30:18.456Z',
+    bornHost: '10.0.1.41:55100',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'notif-004',
+      TAGS: 'push',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A498D2',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 1048576,
+  },
+
+  // ── inventory-sync (4 messages) ──
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4A2E3',
+    topic: 'inventory-sync',
+    tag: 'sync',
+    key: 'inv-sku001',
+    body: '{"skuId":"SKU001","warehouse":"HZ-01","before":500,"after":498,"change":-2,"reason":"order_deduction","orderId":"20240101001"}',
+    storeTime: '2026-07-01T10:24:10.789Z',
+    bornHost: '10.0.1.50:55200',
+    storeHost: '10.0.2.4:10911',
+    properties: {
+      KEYS: 'inv-sku001',
+      TAGS: 'sync',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4A2E3',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 448,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4ACF4',
+    topic: 'inventory-sync',
+    tag: 'deduct',
+    key: 'inv-sku003',
+    body: '{"skuId":"SKU003","warehouse":"SH-02","before":120,"after":119,"change":-1,"reason":"order_deduction","orderId":"20240101004"}',
+    storeTime: '2026-07-01T11:16:15.012Z',
+    bornHost: '10.0.1.51:55201',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'inv-sku003',
+      TAGS: 'deduct',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4ACF4',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 436,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4B705',
+    topic: 'inventory-sync',
+    tag: 'restock',
+    key: 'inv-sku015',
+    body: '{"skuId":"SKU015","warehouse":"HZ-01","before":50,"after":200,"change":150,"reason":"supplier_restock","poId":"PO20240101001"}',
+    storeTime: '2026-07-01T14:30:05.345Z',
+    bornHost: '10.0.1.50:55202',
+    storeHost: '10.0.2.5:10911',
+    properties: {
+      KEYS: 'inv-sku015',
+      TAGS: 'restock',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4B705',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 464,
+  },
+  {
+    msgId: 'AC1E0A6400002A9F0000000001A4C116',
+    topic: 'inventory-sync',
+    tag: 'alert',
+    key: 'inv-sku022',
+    body: '{"skuId":"SKU022","warehouse":"BJ-03","currentStock":3,"threshold":10,"alertType":"low_stock","action":"notify_procurement"}',
+    storeTime: '2026-07-01T16:00:22.678Z',
+    bornHost: '10.0.1.52:55200',
+    storeHost: '10.0.2.3:10911',
+    properties: {
+      KEYS: 'inv-sku022',
+      TAGS: 'alert',
+      UNIQ_KEY: 'AC1E0A6400002A9F0000000001A4C116',
+      WAIT: 'true',
+      MSG_REGION: 'DefaultRegion',
+    },
+    size: 392,
+  },
+];
+
+export const mockMessageTraces: Record<string, TraceRecord> = {
+  // ── order-create traces ──
+  AC1E0A6400002A9F0000000001A3F2B1: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:23:45.100Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'order-service (10.0.1.12:54321) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:23:45.115Z',
+        status: 'finish',
+        costTime: 8,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:23:45.230Z',
+        status: 'finish',
+        costTime: 107,
+        description: 'cg-order-processor → 消费成功 (23ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-order-processor',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:23:45.230Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-notification',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:23:45.450Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-analytics',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:23:46.100Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A3F7C2: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:25:12.400Z',
+        status: 'finish',
+        costTime: 2,
+        description: 'order-service (10.0.1.12:54322) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:25:12.430Z',
+        status: 'finish',
+        costTime: 6,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:25:12.556Z',
+        status: 'finish',
+        costTime: 120,
+        description: 'cg-order-processor → 消费成功 (18ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-order-processor',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:25:12.556Z',
+        retryCount: 0,
+      },
+      { group: 'cg-notification', deliveryStatus: 'pending', consumeTime: '-', retryCount: 0 },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A401D3: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:30:05.700Z',
+        status: 'finish',
+        costTime: 4,
+        description: 'order-service (10.0.1.13:54321) → broker-hz-02',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:30:05.750Z',
+        status: 'finish',
+        costTime: 12,
+        description: 'broker-hz-02 (10.0.2.4:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:30:06.500Z',
+        status: 'finish',
+        costTime: 750,
+        description: 'cg-order-processor → 首次超时,重试成功 (156ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-order-processor',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:30:06.500Z',
+        retryCount: 1,
+      },
+      {
+        group: 'cg-analytics',
+        deliveryStatus: 'failed',
+        consumeTime: '2026-07-01T10:30:08.000Z',
+        retryCount: 3,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A40BE4: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T11:15:33.200Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'order-service (10.0.1.12:54323) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T11:15:33.220Z',
+        status: 'finish',
+        costTime: 5,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T11:15:33.300Z',
+        status: 'finish',
+        costTime: 78,
+        description: 'cg-order-processor → 消费成功 (15ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-order-processor',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T11:15:33.300Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A415F5: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T12:00:18.500Z',
+        status: 'finish',
+        costTime: 5,
+        description: 'order-service (10.0.1.14:54321) → broker-hz-03',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T12:00:18.540Z',
+        status: 'finish',
+        costTime: 9,
+        description: 'broker-hz-03 (10.0.2.5:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T12:00:18.667Z',
+        status: 'finish',
+        costTime: 122,
+        description: 'cg-order-processor → 消费成功 (32ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-order-processor',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T12:00:18.667Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-notification',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T12:00:19.100Z',
+        retryCount: 0,
+      },
+    ],
+  },
+
+  // ── payment-callback traces ──
+  AC1E0A6400002A9F0000000001A42006: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:24:05.800Z',
+        status: 'finish',
+        costTime: 4,
+        description: 'payment-service (10.0.1.20:54400) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:24:05.850Z',
+        status: 'finish',
+        costTime: 10,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:24:06.000Z',
+        status: 'finish',
+        costTime: 145,
+        description: 'cg-payment-handler → 消费成功 (45ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-payment-handler',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:24:06.000Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-order-processor',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:24:06.200Z',
+        retryCount: 0,
+      },
+      { group: 'cg-analytics', deliveryStatus: 'pending', consumeTime: '-', retryCount: 0 },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A42A17: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:26:18.100Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'payment-service (10.0.1.20:54401) → broker-hz-02',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:26:18.110Z',
+        status: 'finish',
+        costTime: 7,
+        description: 'broker-hz-02 (10.0.2.4:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:26:18.200Z',
+        status: 'finish',
+        costTime: 88,
+        description: 'cg-payment-handler → 消费成功 (22ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-payment-handler',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:26:18.200Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-order-processor',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:26:18.500Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A43428: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:32:45.400Z',
+        status: 'finish',
+        costTime: 5,
+        description: 'payment-service (10.0.1.21:54400) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:32:45.440Z',
+        status: 'finish',
+        costTime: 8,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:32:46.000Z',
+        status: 'finish',
+        costTime: 560,
+        description: 'cg-payment-handler → 消费失败 (余额不足)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-payment-handler',
+        deliveryStatus: 'failed',
+        consumeTime: '2026-07-01T10:32:46.000Z',
+        retryCount: 3,
+      },
+      { group: 'cg-order-processor', deliveryStatus: 'pending', consumeTime: '-', retryCount: 0 },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A43E39: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T11:16:08.700Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'payment-service (10.0.1.20:54402) → broker-hz-03',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T11:16:08.760Z',
+        status: 'finish',
+        costTime: 11,
+        description: 'broker-hz-03 (10.0.2.5:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T11:16:09.000Z',
+        status: 'finish',
+        costTime: 235,
+        description: 'cg-payment-handler → 消费成功 (68ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-payment-handler',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T11:16:09.000Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A4484A: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T12:05:22.000Z',
+        status: 'finish',
+        costTime: 4,
+        description: 'payment-service (10.0.1.21:54401) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T12:05:22.008Z',
+        status: 'finish',
+        costTime: 6,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T12:05:22.120Z',
+        status: 'finish',
+        costTime: 110,
+        description: 'cg-payment-handler → 消费成功 (退款处理)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-payment-handler',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T12:05:22.120Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-notification',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T12:05:22.500Z',
+        retryCount: 0,
+      },
+    ],
+  },
+
+  // ── user-activity-log traces ──
+  AC1E0A6400002A9F0000000001A4525B: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T09:15:01.300Z',
+        status: 'finish',
+        costTime: 2,
+        description: 'user-service (10.0.1.30:55000) → broker-hz-02',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T09:15:01.330Z',
+        status: 'finish',
+        costTime: 5,
+        description: 'broker-hz-02 (10.0.2.4:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T09:15:01.445Z',
+        status: 'finish',
+        costTime: 112,
+        description: 'cg-user-tracking → 消费成功 (8ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-user-tracking',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T09:15:01.445Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-risk-control',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T09:15:01.600Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A45C6C: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T09:30:12.600Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'user-service (10.0.1.31:55001) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T09:30:12.650Z',
+        status: 'finish',
+        costTime: 7,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T09:30:12.778Z',
+        status: 'finish',
+        costTime: 125,
+        description: 'cg-user-tracking → 消费成功 (12ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-user-tracking',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T09:30:12.778Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A4667D: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:05:33.800Z',
+        status: 'finish',
+        costTime: 4,
+        description: 'user-service (10.0.1.30:55002) → broker-hz-03',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:05:33.870Z',
+        status: 'finish',
+        costTime: 9,
+        description: 'broker-hz-03 (10.0.2.5:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:05:34.000Z',
+        status: 'finish',
+        costTime: 125,
+        description: 'cg-recommend → 首次失败,重试成功 (35ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-recommend',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:05:34.000Z',
+        retryCount: 1,
+      },
+      {
+        group: 'cg-user-tracking',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:05:33.980Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A4708E: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:15:45.200Z',
+        status: 'finish',
+        costTime: 2,
+        description: 'user-service (10.0.1.30:55000) → broker-hz-02',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:15:45.220Z',
+        status: 'finish',
+        costTime: 4,
+        description: 'broker-hz-02 (10.0.2.4:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:15:45.334Z',
+        status: 'finish',
+        costTime: 112,
+        description: 'cg-user-tracking → 消费成功 (6ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-user-tracking',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:15:45.334Z',
+        retryCount: 0,
+      },
+    ],
+  },
+
+  // ── notification-push traces ──
+  AC1E0A6400002A9F0000000001A47A9F: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T13:00:05.500Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'notification-service (10.0.1.40:55100) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T13:00:05.550Z',
+        status: 'finish',
+        costTime: 8,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T13:00:05.800Z',
+        status: 'finish',
+        costTime: 248,
+        description: 'cg-push-service → 推送成功 (56ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-push-service',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T13:00:05.800Z',
+        retryCount: 0,
+      },
+      { group: 'cg-sms-gateway', deliveryStatus: 'pending', consumeTime: '-', retryCount: 0 },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A484B0: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T13:10:22.800Z',
+        status: 'finish',
+        costTime: 4,
+        description: 'notification-service (10.0.1.41:55101) → broker-hz-02',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T13:10:22.860Z',
+        status: 'finish',
+        costTime: 6,
+        description: 'broker-hz-02 (10.0.2.4:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T13:10:23.500Z',
+        status: 'process',
+        costTime: 640,
+        description: 'cg-sms-gateway → 发送中...',
+      },
+    ],
+    consumerStatus: [
+      { group: 'cg-sms-gateway', deliveryStatus: 'pending', consumeTime: '-', retryCount: 0 },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A48EC1: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T14:00:45.100Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'notification-service (10.0.1.40:55102) → broker-hz-03',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T14:00:45.110Z',
+        status: 'finish',
+        costTime: 5,
+        description: 'broker-hz-03 (10.0.2.5:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T14:00:45.500Z',
+        status: 'finish',
+        costTime: 388,
+        description: 'cg-email-service → 发送成功 (120ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-email-service',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T14:00:45.500Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A498D2: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T15:30:18.400Z',
+        status: 'finish',
+        costTime: 5,
+        description: 'notification-service (10.0.1.41:55100) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T15:30:18.440Z',
+        status: 'finish',
+        costTime: 10,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T15:30:19.000Z',
+        status: 'finish',
+        costTime: 558,
+        description: 'cg-push-service → 首次超时,重试成功 (89ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-push-service',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T15:30:19.000Z',
+        retryCount: 1,
+      },
+    ],
+  },
+
+  // ── inventory-sync traces ──
+  AC1E0A6400002A9F0000000001A4A2E3: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T10:24:10.700Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'inventory-service (10.0.1.50:55200) → broker-hz-02',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T10:24:10.770Z',
+        status: 'finish',
+        costTime: 12,
+        description: 'broker-hz-02 (10.0.2.4:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T10:24:10.889Z',
+        status: 'finish',
+        costTime: 116,
+        description: 'cg-inventory-sync → 同步成功 (15ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-inventory-sync',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:24:10.889Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-warehouse',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T10:24:11.200Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A4ACF4: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T11:16:15.000Z',
+        status: 'finish',
+        costTime: 4,
+        description: 'inventory-service (10.0.1.51:55201) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T11:16:15.008Z',
+        status: 'finish',
+        costTime: 6,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T11:16:15.200Z',
+        status: 'finish',
+        costTime: 190,
+        description: 'cg-warehouse → 同步成功 (28ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-warehouse',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T11:16:15.200Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A4B705: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T14:30:05.300Z',
+        status: 'finish',
+        costTime: 3,
+        description: 'inventory-service (10.0.1.50:55202) → broker-hz-03',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T14:30:05.330Z',
+        status: 'finish',
+        costTime: 7,
+        description: 'broker-hz-03 (10.0.2.5:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T14:30:05.500Z',
+        status: 'finish',
+        costTime: 168,
+        description: 'cg-procurement → 同步成功 (22ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-procurement',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T14:30:05.500Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-inventory-sync',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T14:30:05.600Z',
+        retryCount: 0,
+      },
+    ],
+  },
+  AC1E0A6400002A9F0000000001A4C116: {
+    nodes: [
+      {
+        title: 'Producer 发送',
+        timestamp: '2026-07-01T16:00:22.600Z',
+        status: 'finish',
+        costTime: 5,
+        description: 'inventory-service (10.0.1.52:55200) → broker-hz-01',
+      },
+      {
+        title: 'Broker 存储',
+        timestamp: '2026-07-01T16:00:22.660Z',
+        status: 'finish',
+        costTime: 14,
+        description: 'broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功',
+      },
+      {
+        title: 'Consumer 消费',
+        timestamp: '2026-07-01T16:00:23.000Z',
+        status: 'finish',
+        costTime: 336,
+        description: 'cg-alert-service → 告警已触发 (45ms)',
+      },
+    ],
+    consumerStatus: [
+      {
+        group: 'cg-alert-service',
+        deliveryStatus: 'success',
+        consumeTime: '2026-07-01T16:00:23.000Z',
+        retryCount: 0,
+      },
+      {
+        group: 'cg-inventory-sync',
+        deliveryStatus: 'failed',
+        consumeTime: '2026-07-01T16:00:25.000Z',
+        retryCount: 2,
+      },
+    ],
+  },
+};
diff --git a/web/src/mock/topics.ts b/web/src/mock/topics.ts
new file mode 100644
index 0000000..949097c
--- /dev/null
+++ b/web/src/mock/topics.ts
@@ -0,0 +1,434 @@
+/*
+ * 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.
+ */
+
+export interface Topic {
+  name: string;
+  namespace: string;
+  type: 'NORMAL' | 'FIFO' | 'DELAY' | 'TRANSACTION' | 'LITE';
+  clusterId: string;
+  writeQueues: number;
+  readQueues: number;
+  perm: 'RW' | 'RO' | 'WO';
+  messageCount: number;
+  tps: number;
+  consumerGroupCount: number;
+  remark: string;
+  createdAt: string;
+  updatedAt: string;
+}
+
+export const topics: Topic[] = [
+  // NORMAL topics (4)
+  {
+    name: 'order-create',
+    namespace: 'trade',
+    type: 'NORMAL',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 16,
+    readQueues: 16,
+    perm: 'RW',
+    messageCount: 1_842_350,
+    tps: 1280,
+    consumerGroupCount: 8,
+    remark: '订单创建事件通知',
+    createdAt: '2024-03-15T08:30:00Z',
+    updatedAt: '2024-03-15T10:15:00Z',
+  },
+  {
+    name: 'user-activity-log',
+    namespace: 'user',
+    type: 'NORMAL',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 16,
+    readQueues: 16,
+    perm: 'RW',
+    messageCount: 5_623_100,
+    tps: 3450,
+    consumerGroupCount: 5,
+    remark: '用户行为日志同步',
+    createdAt: '2024-01-20T10:00:00Z',
+    updatedAt: '2024-01-20T14:30:00Z',
+  },
+  {
+    name: 'system-log',
+    namespace: 'message',
+    type: 'NORMAL',
+    clusterId: 'rmq-cn-v4-prod-02',
+    writeQueues: 8,
+    readQueues: 8,
+    perm: 'RW',
+    messageCount: 12_480_000,
+    tps: 8620,
+    consumerGroupCount: 3,
+    remark: '系统日志收集与分发',
+    createdAt: '2023-11-05T14:20:00Z',
+    updatedAt: '2023-11-06T09:45:00Z',
+  },
+  {
+    name: 'notification-email',
+    namespace: 'message',
+    type: 'NORMAL',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 4,
+    readQueues: 4,
+    perm: 'RW',
+    messageCount: 328_700,
+    tps: 215,
+    consumerGroupCount: 2,
+    remark: '邮件通知触发',
+    createdAt: '2024-06-10T09:15:00Z',
+    updatedAt: '2024-06-10T11:20:00Z',
+  },
+
+  // FIFO topics (2)
+  {
+    name: 'inventory-sync',
+    namespace: 'supply',
+    type: 'FIFO',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 16,
+    readQueues: 16,
+    perm: 'RW',
+    messageCount: 956_400,
+    tps: 680,
+    consumerGroupCount: 4,
+    remark: '库存同步顺序消息',
+    createdAt: '2024-04-22T11:00:00Z',
+    updatedAt: '2024-04-22T15:30:00Z',
+  },
+  {
+    name: 'payment-sequence',
+    namespace: 'trade',
+    type: 'FIFO',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 8,
+    readQueues: 8,
+    perm: 'RW',
+    messageCount: 412_850,
+    tps: 320,
+    consumerGroupCount: 6,
+    remark: '支付流程顺序处理',
+    createdAt: '2024-05-18T16:30:00Z',
+    updatedAt: '2024-05-19T08:45:00Z',
+  },
+
+  // DELAY topics (2)
+  {
+    name: 'notification-push',
+    namespace: 'message',
+    type: 'DELAY',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 16,
+    readQueues: 16,
+    perm: 'RW',
+    messageCount: 2_105_600,
+    tps: 1540,
+    consumerGroupCount: 3,
+    remark: '延迟推送通知调度',
+    createdAt: '2024-02-14T13:45:00Z',
+    updatedAt: '2024-02-14T16:20:00Z',
+  },
+  {
+    name: 'scheduled-task',
+    namespace: 'supply',
+    type: 'DELAY',
+    clusterId: 'rmq-cn-v4-prod-02',
+    writeQueues: 4,
+    readQueues: 4,
+    perm: 'RW',
+    messageCount: 87_300,
+    tps: 56,
+    consumerGroupCount: 2,
+    remark: '定时任务触发器',
+    createdAt: '2024-07-01T08:00:00Z',
+    updatedAt: '2024-07-01T10:30:00Z',
+  },
+
+  // TRANSACTION topics (2)
+  {
+    name: 'payment-callback',
+    namespace: 'trade',
+    type: 'TRANSACTION',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 16,
+    readQueues: 16,
+    perm: 'RW',
+    messageCount: 634_200,
+    tps: 420,
+    consumerGroupCount: 5,
+    remark: '支付回调事务处理',
+    createdAt: '2024-03-28T10:30:00Z',
+    updatedAt: '2024-03-28T14:15:00Z',
+  },
+  {
+    name: 'order-confirm',
+    namespace: 'trade',
+    type: 'TRANSACTION',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 8,
+    readQueues: 8,
+    perm: 'RW',
+    messageCount: 521_800,
+    tps: 360,
+    consumerGroupCount: 4,
+    remark: '订单确认事务消息',
+    createdAt: '2024-04-05T15:20:00Z',
+    updatedAt: '2024-04-05T18:40:00Z',
+  },
+
+  // LITE topics (2)
+  {
+    name: 'chat-session',
+    namespace: 'ai',
+    type: 'LITE',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 16,
+    readQueues: 16,
+    perm: 'RW',
+    messageCount: 8_920_000,
+    tps: 6200,
+    consumerGroupCount: 1,
+    remark: 'AI 对话会话消息分发',
+    createdAt: '2025-01-10T09:00:00Z',
+    updatedAt: '2025-01-10T11:30:00Z',
+  },
+  {
+    name: 'ai-task-dispatch',
+    namespace: 'ai',
+    type: 'LITE',
+    clusterId: 'rmq-cn-v5-prod-01',
+    writeQueues: 16,
+    readQueues: 16,
+    perm: 'RW',
+    messageCount: 3_450_000,
+    tps: 2380,
+    consumerGroupCount: 2,
+    remark: 'AI 任务调度与状态同步',
+    createdAt: '2025-02-20T14:00:00Z',
+    updatedAt: '2025-02-20T16:45:00Z',
+  },
+];
+
+// Mock route info for drawer
+export interface BrokerRoute {
+  brokerName: string;
+  brokerAddr: string;
+  writeQueues: number;
+  readQueues: number;
+  perm: string;
+}
+
+export const topicRoutes: Record<string, BrokerRoute[]> = {
+  'order-create': [
+    {
+      brokerName: 'broker-a-0',
+      brokerAddr: '10.0.1.10:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+    {
+      brokerName: 'broker-b-0',
+      brokerAddr: '10.0.1.11:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+  ],
+  'user-activity-log': [
+    {
+      brokerName: 'broker-a-0',
+      brokerAddr: '10.0.1.10:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+    {
+      brokerName: 'broker-b-0',
+      brokerAddr: '10.0.1.11:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+  ],
+  'system-log': [
+    {
+      brokerName: 'broker-a-0',
+      brokerAddr: '10.0.2.10:10911',
+      writeQueues: 4,
+      readQueues: 4,
+      perm: 'RW',
+    },
+    {
+      brokerName: 'broker-b-0',
+      brokerAddr: '10.0.2.11:10911',
+      writeQueues: 4,
+      readQueues: 4,
+      perm: 'RW',
+    },
+  ],
+  'inventory-sync': [
+    {
+      brokerName: 'broker-a-0',
+      brokerAddr: '10.0.1.10:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+    {
+      brokerName: 'broker-b-0',
+      brokerAddr: '10.0.1.11:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+  ],
+  'payment-callback': [
+    {
+      brokerName: 'broker-a-0',
+      brokerAddr: '10.0.1.10:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+    {
+      brokerName: 'broker-b-0',
+      brokerAddr: '10.0.1.11:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+    },
+  ],
+};
+
+// Default route for topics not explicitly listed
+export const defaultRoute: BrokerRoute[] = [
+  {
+    brokerName: 'broker-a-0',
+    brokerAddr: '10.0.1.10:10911',
+    writeQueues: 8,
+    readQueues: 8,
+    perm: 'RW',
+  },
+  {
+    brokerName: 'broker-b-0',
+    brokerAddr: '10.0.1.11:10911',
+    writeQueues: 8,
+    readQueues: 8,
+    perm: 'RW',
+  },
+];
+
+// Mock consumer groups for topics
+export interface ConsumerGroupInfo {
+  group: string;
+  consumeType: 'CLUSTERING' | 'BROADCASTING';
+  messageModel: string;
+  consumeTps: number;
+  diffTotal: number;
+}
+
+export const topicConsumers: Record<string, ConsumerGroupInfo[]> = {
+  'order-create': [
+    {
+      group: 'GID_order_service',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 640,
+      diffTotal: 120,
+    },
+    {
+      group: 'GID_inventory_deduction',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 320,
+      diffTotal: 0,
+    },
+    {
+      group: 'GID_risk_control',
+      consumeType: 'BROADCASTING',
+      messageModel: '广播消费',
+      consumeTps: 180,
+      diffTotal: 45,
+    },
+    {
+      group: 'GID_notification_trigger',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 140,
+      diffTotal: 0,
+    },
+  ],
+  'user-activity-log': [
+    {
+      group: 'GID_user_profile_sync',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 2100,
+      diffTotal: 350,
+    },
+    {
+      group: 'GID_recommendation_engine',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 1350,
+      diffTotal: 0,
+    },
+  ],
+  'payment-callback': [
+    {
+      group: 'GID_payment_service',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 280,
+      diffTotal: 0,
+    },
+    {
+      group: 'GID_order_status_update',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 140,
+      diffTotal: 12,
+    },
+  ],
+  'inventory-sync': [
+    {
+      group: 'GID_warehouse_sync',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 340,
+      diffTotal: 0,
+    },
+    {
+      group: 'GID_stock_alert',
+      consumeType: 'CLUSTERING',
+      messageModel: '集群消费',
+      consumeTps: 340,
+      diffTotal: 88,
+    },
+  ],
+};
+
+export const defaultConsumers: ConsumerGroupInfo[] = [
+  {
+    group: 'GID_default_consumer',
+    consumeType: 'CLUSTERING',
+    messageModel: '集群消费',
+    consumeTps: 100,
+    diffTotal: 0,
+  },
+];
diff --git a/web/src/pages/ai/index.tsx b/web/src/pages/ai/index.tsx
new file mode 100644
index 0000000..8c8e38a
--- /dev/null
+++ b/web/src/pages/ai/index.tsx
@@ -0,0 +1,630 @@
+/*
+ * 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 { useState, useRef, useEffect, useCallback } from 'react';
+import {
+  Card,
+  Button,
+  Tag,
+  Typography,
+  Table,
+  Statistic,
+  Row,
+  Col,
+  Descriptions,
+  Flex,
+  Divider,
+  Select,
+} from 'antd';
+import { ArrowUp, Sparkle, SlidersHorizontal, CaretDown } from '@phosphor-icons/react';
+import type { ColumnsType } from 'antd/es/table';
+import { useLang } from '../../i18n/LangContext';
+
+const { Text, Paragraph } = Typography;
+
+/* ─── Types ─── */
+
+interface ToolCallTag {
+  name: string;
+  label: string;
+}
+
+interface TopicRow {
+  key: string;
+  name: string;
+  type: string;
+  queues: number;
+}
+
+interface StatItem {
+  title: string;
+  value: string | number;
+  suffix?: string;
+  color: string;
+}
+
+interface DescriptionItem {
+  label: string;
+  value: string;
+}
+
+interface Message {
+  id: string;
+  role: 'user' | 'ai';
+  text?: string;
+  toolCall?: ToolCallTag;
+  tableData?: TopicRow[];
+  tableColumns?: ColumnsType<TopicRow>;
+  stats?: StatItem[];
+  descriptions?: DescriptionItem[];
+  summary?: string;
+  actions?: { label: string; type?: 'primary' | 'default' }[];
+}
+
+/* ─── Model Options ─── */
+
+const modelOptions = [
+  {
+    value: 'qwen3.7-max',
+    label: (
+      <span className="inline-flex items-center gap-1.5">
+        Qwen3.7-Max
+        <span className="px-1 py-0.5 rounded text-[0.625rem] leading-none bg-purple-50 text-purple-600 font-medium">
+          推荐
+        </span>
+      </span>
+    ),
+  },
+  { value: 'qwen3.7-plus', label: 'Qwen3.7-Plus' },
+  { value: 'claude-opus-4.7', label: 'Claude Opus 4.7' },
+  { value: 'gpt-5.4', label: 'GPT-5.4' },
+];
+
+/* ─── Mock Data ─── */
+
+const topicColumns: ColumnsType<TopicRow> = [
+  { title: 'Topic 名称', dataIndex: 'name', key: 'name' },
+  {
+    title: '类型',
+    dataIndex: 'type',
+    key: 'type',
+    render: (type: string) => {
+      const map: Record<string, { label: string; color: string }> = {
+        NORMAL: { label: '普通', color: 'default' },
+        FIFO: { label: '顺序', color: 'blue' },
+        DELAY: { label: '延迟', color: 'orange' },
+        TRANSACTION: { label: '事务', color: 'purple' },
+      };
+      const cfg = map[type] || { label: type, color: 'default' };
+      return <Tag color={cfg.color}>{cfg.label}</Tag>;
+    },
+  },
+  { title: '队列数', dataIndex: 'queues', key: 'queues', width: 80, align: 'center' },
+];
+
+const mockTopicData: TopicRow[] = [
+  { key: '1', name: 'order-create', type: 'TRANSACTION', queues: 16 },
+  { key: '2', name: 'payment-notify', type: 'NORMAL', queues: 12 },
+  { key: '3', name: 'user-login-event', type: 'NORMAL', queues: 8 },
+  { key: '4', name: 'inventory-sync', type: 'FIFO', queues: 8 },
+  { key: '5', name: 'promo-push', type: 'DELAY', queues: 4 },
+];
+
+const initialMessages: Message[] = [
+  {
+    id: 'm1',
+    role: 'user',
+    text: '查看生产集群-杭州的 Topic 列表',
+  },
+  {
+    id: 'm2',
+    role: 'ai',
+    toolCall: { name: 'list_topics', label: '🔧 list_topics' },
+    tableData: mockTopicData,
+    tableColumns: topicColumns,
+    summary: '生产集群-杭州 共有 128 个 Topic,以上为按吞吐量排序的前 5 个。',
+  },
+  {
+    id: 'm3',
+    role: 'user',
+    text: 'order-create 这个 Topic 最近 1 小时的堆积情况',
+  },
+  {
+    id: 'm4',
+    role: 'ai',
+    toolCall: { name: 'get_topic_lag', label: '🔧 get_topic_lag' },
+    stats: [
+      { title: '当前堆积', value: '2,340', color: '#1677ff' },
+      { title: '消费速率', value: '856', suffix: '/s', color: '#52c41a' },
+      { title: '预计追平', value: '2.7', suffix: 's', color: '#722ed1' },
+    ],
+    summary: 'order-create 消费状态良好,堆积量较低,消费速率稳定。',
+  },
+  {
+    id: 'm5',
+    role: 'user',
+    text: '帮我创建一个事务类型的 Topic,名称 order-refund',
+  },
+  {
+    id: 'm6',
+    role: 'ai',
+    toolCall: { name: 'create_topic', label: '🔧 create_topic (dry-run)' },
+    descriptions: [
+      { label: '名称', value: 'order-refund' },
+      { label: '类型', value: 'TRANSACTION' },
+      { label: '队列数', value: '16' },
+      { label: '集群', value: '生产集群-杭州' },
+    ],
+    summary: '已生成创建预览,确认后将执行创建操作。',
+    actions: [
+      { label: '确认创建', type: 'primary' },
+      { label: '取消', type: 'default' },
+    ],
+  },
+];
+
+/* ─── Quick Actions ─── */
+
+const quickActions = [
+  '查看集群状态',
+  'Topic 堆积 Top10',
+  '诊断消费延迟',
+  '创建 Topic',
+  '消息轨迹查询',
+  '扩缩容评估',
+];
+
+/* ─── Sub-components ─── */
+
+const UserBubble = ({ text }: { text: string }) => (
+  <Flex justify="flex-end" style={{ marginBottom: 16 }}>
+    <div
+      style={{
+        maxWidth: '70%',
+        padding: '10px 16px',
+        background: '#e6f4ff',
+        borderRadius: 16,
+        borderTopRightRadius: 4,
+        lineHeight: 1.6,
+        fontSize: 14,
+      }}
+    >
+      {text}
+    </div>
+  </Flex>
+);
+
+const AiMessage = ({ msg }: { msg: Message }) => (
+  <Flex gap={12} align="flex-start" style={{ marginBottom: 16 }}>
+    <div
+      style={{
+        width: 36,
+        height: 36,
+        borderRadius: '50%',
+        background: 'linear-gradient(135deg, #1677ff 0%, #722ed1 100%)',
+        flexShrink: 0,
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        boxShadow: '0 2px 8px rgba(22, 119, 255, 0.3)',
+      }}
+    >
+      <svg
+        width="20"
+        height="20"
+        viewBox="0 0 24 24"
+        fill="none"
+        stroke="white"
+        strokeWidth="2"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      >
+        <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
+        <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
+        <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
+        <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
+      </svg>
+    </div>
+    <Card
+      size="small"
+      style={{
+        maxWidth: '75%',
+        boxShadow: '0 1px 4px rgba(0, 0, 0, 0.06)',
+        borderRadius: 12,
+        borderTopLeftRadius: 4,
+      }}
+      styles={{ body: { padding: '12px 16px' } }}
+    >
+      {/* Tool call indicator */}
+      {msg.toolCall && (
+        <Tag
+          color="purple"
+          style={{
+            marginBottom: 12,
+            borderRadius: 6,
+            fontSize: 12,
+            background: '#f9f0ff',
+            borderColor: '#d3adf7',
+          }}
+        >
+          {msg.toolCall.label}
+        </Tag>
+      )}
+
+      {/* Table content */}
+      {msg.tableData && msg.tableColumns && (
+        <Table
+          dataSource={msg.tableData}
+          columns={msg.tableColumns}
+          rowKey="key"
+          size="small"
+          pagination={false}
+          style={{ marginBottom: 12 }}
+        />
+      )}
+
+      {/* Stat cards */}
+      {msg.stats && (
+        <Row gutter={12} style={{ marginBottom: 12 }}>
+          {msg.stats.map((s) => (
+            <Col key={s.title}>
+              <Card
+                size="small"
+                style={{
+                  borderRadius: 8,
+                  borderTop: `3px solid ${s.color}`,
+                  minWidth: 120,
+                }}
+                styles={{ body: { padding: '8px 12px' } }}
+              >
+                <Statistic
+                  title={
+                    <Text type="secondary" style={{ fontSize: 12 }}>
+                      {s.title}
+                    </Text>
+                  }
+                  value={s.value}
+                  suffix={s.suffix}
+                  valueStyle={{ fontSize: 20, fontWeight: 600, color: s.color }}
+                />
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      )}
+
+      {/* Descriptions */}
+      {msg.descriptions && (
+        <Descriptions bordered size="small" column={2} style={{ marginBottom: 12 }}>
+          {msg.descriptions.map((d) => (
+            <Descriptions.Item key={d.label} label={d.label}>
+              <Text strong>{d.value}</Text>
+            </Descriptions.Item>
+          ))}
+        </Descriptions>
+      )}
+
+      {/* Summary text */}
+      {msg.summary && (
+        <Paragraph style={{ margin: 0, fontSize: 14, color: '#595959' }}>{msg.summary}</Paragraph>
+      )}
+
+      {/* Action buttons */}
+      {msg.actions && (
+        <>
+          <Divider style={{ margin: '12px 0 8px' }} />
+          <Flex gap={8}>
+            {msg.actions.map((a) => (
+              <Button key={a.label} type={a.type || 'default'} size="small">
+                {a.label}
+              </Button>
+            ))}
+          </Flex>
+        </>
+      )}
+    </Card>
+  </Flex>
+);
+
+/* ═══════════════════════════════════════════
+   AiPage
+   ═══════════════════════════════════════════ */
+
+const AiPage = () => {
+  const { t } = useLang();
+  const [messages, setMessages] = useState<Message[]>(initialMessages);
+  const [inputValue, setInputValue] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [selectedModel, setSelectedModel] = useState('qwen3.7-max');
+  const chatEndRef = useRef<HTMLDivElement>(null);
+  const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+  const scrollToBottom = useCallback(() => {
+    chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+  }, []);
+
+  useEffect(() => {
+    scrollToBottom();
+  }, [messages, scrollToBottom]);
+
+  /* ─── Auto-resize textarea ─── */
+  useEffect(() => {
+    const ta = textareaRef.current;
+    if (!ta) return;
+    const handler = () => {
+      ta.style.height = 'auto';
+      ta.style.height = `${Math.min(ta.scrollHeight, 300)}px`;
+    };
+    ta.addEventListener('input', handler);
+    return () => ta.removeEventListener('input', handler);
+  }, []);
+
+  const handleSend = useCallback(() => {
+    const text = inputValue.trim();
+    if (!text || loading) return;
+
+    const userMsg: Message = {
+      id: `user-${Date.now()}`,
+      role: 'user',
+      text,
+    };
+
+    setMessages((prev) => [...prev, userMsg]);
+    setInputValue('');
+    if (textareaRef.current) {
+      textareaRef.current.style.height = 'auto';
+    }
+    setLoading(true);
+
+    // Mock AI response after brief delay
+    setTimeout(() => {
+      const aiMsg: Message = {
+        id: `ai-${Date.now()}`,
+        role: 'ai',
+        toolCall: { name: 'process_query', label: '🔧 process_query' },
+        summary: `已收到你的问题:「${text}」。AI 助手正在分析并调用相关 MCP 工具,结果将在此处展示。(当前为演示模式)`,
+      };
+      setMessages((prev) => [...prev, aiMsg]);
+      setLoading(false);
+    }, 1200);
+  }, [inputValue, loading]);
+
+  const handleKeyDown = useCallback(
+    (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+      if (e.key === 'Enter' && !e.shiftKey) {
+        e.preventDefault();
+        handleSend();
+      }
+    },
+    [handleSend],
+  );
+
+  const handleQuickAction = useCallback((action: string) => {
+    setInputValue(action);
+    textareaRef.current?.focus();
+  }, []);
+
+  return (
+    <Flex vertical style={{ height: 'calc(100vh - 48px)', padding: 24 }}>
+      {/* Chat Area */}
+      <div
+        className="w-full scrollbar-hide"
+        style={{
+          flex: 1,
+          overflowY: 'auto',
+          padding: '16px 24px',
+          scrollBehavior: 'smooth',
+        }}
+      >
+        {messages.map((msg) =>
+          msg.role === 'user' ? (
+            <UserBubble key={msg.id} text={msg.text!} />
+          ) : (
+            <AiMessage key={msg.id} msg={msg} />
+          ),
+        )}
+        {loading && (
+          <Flex gap={12} align="flex-start" style={{ marginBottom: 16 }}>
+            <div
+              style={{
+                width: 36,
+                height: 36,
+                borderRadius: '50%',
+                background: 'linear-gradient(135deg, #1677ff 0%, #722ed1 100%)',
+                flexShrink: 0,
+                display: 'flex',
+                alignItems: 'center',
+                justifyContent: 'center',
+                boxShadow: '0 2px 8px rgba(22, 119, 255, 0.3)',
+              }}
+            >
+              <svg
+                width="20"
+                height="20"
+                viewBox="0 0 24 24"
+                fill="none"
+                stroke="white"
+                strokeWidth="2"
+                strokeLinecap="round"
+                strokeLinejoin="round"
+              >
+                <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
+                <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
+                <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
+                <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
+              </svg>
+            </div>
+            <Card
+              size="small"
+              style={{
+                boxShadow: '0 1px 4px rgba(0, 0, 0, 0.06)',
+                borderRadius: 12,
+                borderTopLeftRadius: 4,
+              }}
+              styles={{ body: { padding: '12px 16px' } }}
+            >
+              <Flex gap={4} align="center">
+                <span
+                  style={{
+                    display: 'inline-block',
+                    width: 6,
+                    height: 6,
+                    borderRadius: '50%',
+                    background: '#722ed1',
+                    animation: 'dotPulse 1.4s infinite ease-in-out',
+                  }}
+                />
+                <span
+                  style={{
+                    display: 'inline-block',
+                    width: 6,
+                    height: 6,
+                    borderRadius: '50%',
+                    background: '#722ed1',
+                    animation: 'dotPulse 1.4s infinite ease-in-out 0.2s',
+                  }}
+                />
+                <span
+                  style={{
+                    display: 'inline-block',
+                    width: 6,
+                    height: 6,
+                    borderRadius: '50%',
+                    background: '#722ed1',
+                    animation: 'dotPulse 1.4s infinite ease-in-out 0.4s',
+                  }}
+                />
+              </Flex>
+            </Card>
+          </Flex>
+        )}
+        <div ref={chatEndRef} />
+      </div>
+
+      {/* Input Area */}
+      <div className="w-full" style={{ flexShrink: 0, padding: '0 24px' }}>
+        {/* Quick Actions */}
+        <Flex align="center" gap={8} style={{ marginBottom: 12 }} wrap="wrap">
+          <Text type="secondary" style={{ fontSize: 13, flexShrink: 0 }}>
+            {t('ai.commonCommands')}
+          </Text>
+          {quickActions.map((action) => (
+            <Tag
+              key={action}
+              style={{
+                cursor: 'pointer',
+                borderRadius: 6,
+                padding: '2px 10px',
+                fontSize: 13,
+                userSelect: 'none',
+                transition: 'all 0.2s',
+              }}
+              color="blue"
+              onClick={() => handleQuickAction(action)}
+            >
+              {action}
+            </Tag>
+          ))}
+        </Flex>
+
+        {/* Main Input Box */}
+        <div className="relative overflow-visible border-[1.5px] backdrop-blur-xl border-white rounded-2xl bg-white/80 shadow-[0_20px_60px_-20px_rgba(80,90,180,0.18)]">
+          {/* Model Selector */}
+          <div className="flex items-center justify-between gap-3 px-3.5 pt-4">
+            <div className="flex flex-1 min-w-0 items-center gap-2">
+              <Select
+                size="small"
+                value={selectedModel}
+                onChange={(val) => setSelectedModel(val)}
+                options={modelOptions}
+                variant="borderless"
+                popupMatchSelectWidth={false}
+                suffixIcon={<CaretDown size={10} color="#9CA3AF" />}
+                className="model-selector"
+                style={{ fontSize: '0.893rem' }}
+              />
+            </div>
+          </div>
+
+          {/* Textarea */}
+          <div className="relative flex flex-col">
+            <textarea
+              ref={textareaRef}
+              className="chat-input"
+              value={inputValue}
+              onChange={(e) => setInputValue(e.target.value)}
+              onKeyDown={handleKeyDown}
+              placeholder="输入你的问题或指令,例如:查看集群状态、创建 Topic、诊断消费延迟..."
+            />
+            <Sparkle
+              className="text-gray-400"
+              style={{
+                position: 'absolute',
+                top: 18,
+                left: 26,
+                fontSize: 17,
+              }}
+            />
+          </div>
+
+          {/* Bottom Toolbar */}
+          <div className="flex justify-between text-sm items-center px-3.5 py-3 border-t border-gray-100/80">
+            <div className="flex flex-1 gap-1 items-center min-w-0">
+              <div className="flex items-center gap-2 w-full">
+                <div className="flex-1 min-w-0">
+                  <div className="flex items-center gap-2 overflow-x-auto scrollbar-hide max-w-full py-2">
+                    <button className="tool-btn">
+                      <SlidersHorizontal size={17} />
+                      <span>工具</span>
+                    </button>
+                    <button className="tool-btn">
+                      <Sparkle size={17} />
+                      <span>Prompt 增强</span>
+                    </button>
+                  </div>
+                </div>
+                <div className="shrink-0 flex items-center gap-1">
+                  <button
+                    className="flex items-center justify-center w-9 h-9 rounded-full bg-gradient-to-r from-purple-500 to-violet-600 text-white shadow-lg hover:shadow-xl transition-all hover:scale-105"
+                    onClick={handleSend}
+                    disabled={loading || !inputValue.trim()}
+                    style={{
+                      opacity: loading || !inputValue.trim() ? 0.5 : 1,
+                      cursor: loading || !inputValue.trim() ? 'not-allowed' : 'pointer',
+                    }}
+                  >
+                    <ArrowUp size={19} weight="bold" />
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      {/* Keyframes for loading animation */}
+      <style>{`
+        @keyframes dotPulse {
+          0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
+          40% { opacity: 1; transform: scale(1.2); }
+        }
+      `}</style>
+    </Flex>
+  );
+};
+
+export default AiPage;
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
new file mode 100644
index 0000000..8449357
--- /dev/null
+++ b/web/src/pages/cluster/certs.tsx
@@ -0,0 +1,298 @@
+/*
+ * 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 { useState } from 'react';
+import {
+  Table,
+  Tag,
+  Button,
+  Input,
+  Select,
+  Modal,
+  Form,
+  Flex,
+  Space,
+  Typography,
+  Card,
+  message,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import { EditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
+import PageHeader from '../../components/PageHeader';
+import { mockK8sCerts, type K8sCertInfo } from '../../mock/clusters';
+
+const { Text } = Typography;
+
+const formatDateTime = (iso: string): string => {
+  const d = new Date(iso);
+  const pad = (n: number) => String(n).padStart(2, '0');
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+};
+
+const K8sCertsPage = () => {
+  const [certs, setCerts] = useState<K8sCertInfo[]>(mockK8sCerts);
+  const [certSearch, setCertSearch] = useState('');
+  const [certTypeFilter, setCertTypeFilter] = useState<string>('');
+  const [editModalOpen, setEditModalOpen] = useState(false);
+  const [editingCert, setEditingCert] = useState<K8sCertInfo | null>(null);
+  const [editForm] = Form.useForm();
+
+  const filteredCerts = certs.filter((cert) => {
+    const matchSearch =
+      !certSearch ||
+      cert.name.toLowerCase().includes(certSearch.toLowerCase()) ||
+      cert.cluster.toLowerCase().includes(certSearch.toLowerCase());
+    const matchType = !certTypeFilter || cert.type === certTypeFilter;
+    return matchSearch && matchType;
+  });
+
+  const certColumns: ColumnsType<K8sCertInfo> = [
+    {
+      title: 'K8s 集群名称',
+      dataIndex: 'cluster',
+      key: 'cluster',
+      width: 160,
+      sorter: (a, b) => a.cluster.localeCompare(b.cluster),
+      render: (name: string) => <Text strong>{name}</Text>,
+    },
+    {
+      title: '证书名称',
+      dataIndex: 'name',
+      key: 'name',
+      width: 280,
+      sorter: (a, b) => a.name.localeCompare(b.name),
+      render: (name: string) => (
+        <Text style={{ fontFamily: 'monospace', fontSize: 13 }}>{name}</Text>
+      ),
+    },
+    {
+      title: '类型',
+      dataIndex: 'type',
+      key: 'type',
+      width: 130,
+      sorter: (a, b) => a.type.localeCompare(b.type),
+      render: (type: string) => {
+        const colorMap: Record<string, string> = {
+          TLS: 'blue',
+          mTLS: 'purple',
+          ServiceAccount: 'orange',
+        };
+        return <Tag color={colorMap[type] ?? 'default'}>{type}</Tag>;
+      },
+    },
+    {
+      title: '签发者',
+      dataIndex: 'issuer',
+      key: 'issuer',
+      width: 130,
+      sorter: (a, b) => a.issuer.localeCompare(b.issuer),
+    },
+    {
+      title: '到期时间',
+      dataIndex: 'notAfter',
+      key: 'notAfter',
+      width: 170,
+      sorter: (a, b) => new Date(a.notAfter).getTime() - new Date(b.notAfter).getTime(),
+      render: (iso: string) => (
+        <Text type="secondary" style={{ fontSize: 13 }}>
+          {formatDateTime(iso)}
+        </Text>
+      ),
+    },
+    {
+      title: '剩余天数',
+      dataIndex: 'daysRemaining',
+      key: 'daysRemaining',
+      width: 100,
+      sorter: (a, b) => a.daysRemaining - b.daysRemaining,
+      render: (days: number) => (
+        <Text
+          style={{
+            color: days <= 0 ? '#ff4d4f' : days <= 30 ? '#faad14' : '#52c41a',
+            fontWeight: 500,
+          }}
+        >
+          {days}
+        </Text>
+      ),
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      width: 100,
+      sorter: (a, b) => a.status.localeCompare(b.status),
+      render: (status: string) => {
+        const map: Record<string, { color: string; label: string }> = {
+          valid: { color: 'green', label: '有效' },
+          expiring: { color: 'orange', label: '即将过期' },
+          expired: { color: 'red', label: '已过期' },
+        };
+        const cfg = map[status] ?? { color: 'default', label: status };
+        return <Tag color={cfg.color}>{cfg.label}</Tag>;
+      },
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      render: (_: unknown, record: K8sCertInfo) => (
+        <Flex gap={6}>
+          <Button
+            size="small"
+            icon={<EditOutlined />}
+            style={{ borderColor: '#1677ff', color: '#1677ff' }}
+            onClick={() => {
+              setEditingCert(record);
+              editForm.setFieldsValue({
+                name: record.name,
+                type: record.type,
+                issuer: record.issuer,
+                namespace: record.namespace,
+              });
+              setEditModalOpen(true);
+            }}
+          >
+            编辑
+          </Button>
+          <Button
+            size="small"
+            icon={<DeleteOutlined />}
+            style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
+            onClick={() => {
+              Modal.confirm({
+                title: '确认删除',
+                content: `确定要删除证书 "${record.name}" 吗?`,
+                okText: '确认',
+                cancelText: '取消',
+                okButtonProps: { danger: true },
+                onOk: () => {
+                  setCerts((prev) => prev.filter((c) => c.id !== record.id));
+                  message.success(`证书已删除: ${record.name}`);
+                },
+              });
+            }}
+          >
+            删除
+          </Button>
+        </Flex>
+      ),
+    },
+  ];
+
+  return (
+    <div style={{ padding: 24 }}>
+      <PageHeader
+        title="K8s 证书管理"
+        subtitle={`共 ${filteredCerts.length} 个证书`}
+        extra={
+          <Button
+            type="primary"
+            icon={<PlusOutlined />}
+            onClick={() => message.info('添加证书功能开发中')}
+          >
+            添加证书
+          </Button>
+        }
+      />
+      <Flex justify="space-between" style={{ marginBottom: 16 }}>
+        <Space>
+          <Input.Search
+            placeholder="搜索证书名称或集群"
+            allowClear
+            onSearch={setCertSearch}
+            onChange={(e) => !e.target.value && setCertSearch('')}
+            style={{ width: 240 }}
+          />
+          <Select
+            value={certTypeFilter}
+            onChange={setCertTypeFilter}
+            style={{ width: 160 }}
+            options={[
+              { value: '', label: '全部' },
+              { value: 'TLS', label: 'TLS' },
+              { value: 'mTLS', label: 'mTLS' },
+              { value: 'ServiceAccount', label: 'ServiceAccount' },
+            ]}
+          />
+        </Space>
+      </Flex>
+      <Card bodyStyle={{ padding: 0 }}>
+        <Table
+          columns={certColumns}
+          dataSource={filteredCerts}
+          rowKey="id"
+          pagination={{ pageSize: 20 }}
+          size="small"
+        />
+      </Card>
+
+      {/* Edit Cert Modal */}
+      <Modal
+        title={`编辑证书 — ${editingCert?.name || ''}`}
+        open={editModalOpen}
+        onCancel={() => {
+          setEditModalOpen(false);
+          editForm.resetFields();
+        }}
+        onOk={() => {
+          editForm.validateFields().then((values) => {
+            if (!editingCert) return;
+            setCerts((prev) =>
+              prev.map((c) =>
+                c.id === editingCert.id
+                  ? { ...c, issuer: values.issuer, namespace: values.namespace }
+                  : c,
+              ),
+            );
+            message.success(`证书「${editingCert.name}」已更新`);
+            setEditModalOpen(false);
+            editForm.resetFields();
+          });
+        }}
+        okText="保存"
+        cancelText="取消"
+        width={520}
+      >
+        <Form form={editForm} layout="vertical" style={{ marginTop: 16 }}>
+          <Form.Item label="证书名称">
+            <Input value={editingCert?.name} disabled />
+          </Form.Item>
+          <Form.Item label="类型">
+            <Select
+              value={editingCert?.type}
+              disabled
+              options={[
+                { value: 'TLS', label: 'TLS' },
+                { value: 'mTLS', label: 'mTLS' },
+                { value: 'ServiceAccount', label: 'ServiceAccount' },
+              ]}
+            />
+          </Form.Item>
+          <Form.Item label="签发者" name="issuer">
+            <Input placeholder="例:kubernetes-ca" />
+          </Form.Item>
+          <Form.Item label="命名空间" name="namespace">
+            <Input placeholder="例:kube-system" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default K8sCertsPage;
diff --git a/web/src/pages/cluster/clients.tsx b/web/src/pages/cluster/clients.tsx
new file mode 100644
index 0000000..04bba37
--- /dev/null
+++ b/web/src/pages/cluster/clients.tsx
@@ -0,0 +1,252 @@
+/*
+ * 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 { useState, useMemo } from 'react';
+import { Table, Card, Tag, Space, Input, Select, Flex, Typography } from 'antd';
+import { MagnifyingGlass } from '@phosphor-icons/react';
+import type { ColumnsType } from 'antd/es/table';
+
+import PageHeader from '../../components/PageHeader';
+import { mockClients } from '../../mock/clients';
+import type { ClientConnection } from '../../mock/clients';
+import clusters from '../../mock/clusters';
+import { useLang } from '../../i18n/LangContext';
+
+const { Text } = Typography;
+
+/* ─── Helpers ─── */
+
+const typeConfig: Record<string, { color: string; label: string }> = {
+  Producer: { color: 'blue', label: 'Producer' },
+  Consumer: { color: 'green', label: 'Consumer' },
+};
+
+const protocolConfig: Record<string, { color: string; label: string }> = {
+  gRPC: { color: 'green', label: 'gRPC' },
+  Remoting: { color: 'blue', label: 'Remoting' },
+};
+
+const languageConfig: Record<string, { color: string; label: string }> = {
+  Java: { color: 'default', label: 'Java' },
+  Go: { color: 'cyan', label: 'Go' },
+  Python: { color: 'purple', label: 'Python' },
+  Rust: { color: 'orange', label: 'Rust' },
+};
+
+/* ═══════════════════════════════════════════
+   ClientsPage
+   ═══════════════════════════════════════════ */
+const ClientsPage = () => {
+  const { t } = useLang();
+  const [search, setSearch] = useState('');
+  const [clusterFilter, setClusterFilter] = useState<string>('ALL');
+
+  /* ─── Cluster options using nsClusterName ─── */
+  const clusterOptions = useMemo(() => {
+    return [
+      { value: 'ALL', label: t('clients.allClusters') },
+      ...clusters.map((c) => ({ value: c.nsClusterName, label: c.nsClusterName })),
+    ];
+  }, []);
+
+  /* ─── Filtered data (search + cluster only, table handles column filters) ─── */
+  const filtered = useMemo(() => {
+    let data = mockClients.filter(
+      (c) => c.clientId.toLowerCase().includes(search.toLowerCase()) || c.address.includes(search),
+    );
+
+    if (clusterFilter !== 'ALL') {
+      data = data.filter((c) => c.clusterName === clusterFilter);
+    }
+
+    return data;
+  }, [search, clusterFilter]);
+
+  /* ═══════════════════════════════════════════
+     Table Columns (with built-in filters)
+     ═══════════════════════════════════════════ */
+  const columns: ColumnsType<ClientConnection> = [
+    {
+      title: t('clients.cluster'),
+      dataIndex: 'clusterName',
+      key: 'clusterName',
+      width: 130,
+      filters: clusters.map((c) => ({ text: c.nsClusterName, value: c.nsClusterName })),
+      onFilter: (value, record) => record.clusterName === value,
+      render: (name: string) => (
+        <Tag color="blue" style={{ fontSize: 12 }}>
+          {name}
+        </Tag>
+      ),
+    },
+    {
+      title: t('clients.clientId'),
+      dataIndex: 'clientId',
+      key: 'clientId',
+      width: 260,
+      render: (id: string) => (
+        <Text
+          copyable
+          style={{
+            fontSize: 13,
+            fontFamily: 'monospace',
+            whiteSpace: 'nowrap',
+          }}
+        >
+          {id}
+        </Text>
+      ),
+    },
+    {
+      title: t('common.type'),
+      dataIndex: 'type',
+      key: 'type',
+      width: 100,
+      filters: [
+        { text: 'Producer', value: 'Producer' },
+        { text: 'Consumer', value: 'Consumer' },
+      ],
+      onFilter: (value, record) => record.type === value,
+      render: (type: string) => {
+        const cfg = typeConfig[type] ?? { label: type };
+        return <Text style={{ fontSize: 13 }}>{cfg.label}</Text>;
+      },
+    },
+    {
+      title: t('clients.groupOrTopic'),
+      dataIndex: 'groupOrTopic',
+      key: 'groupOrTopic',
+      width: 180,
+      render: (name: string) => (
+        <Text strong style={{ fontSize: 13 }}>
+          {name}
+        </Text>
+      ),
+    },
+    {
+      title: t('clients.protocol'),
+      dataIndex: 'protocol',
+      key: 'protocol',
+      width: 110,
+      filters: [
+        { text: 'gRPC', value: 'gRPC' },
+        { text: 'Remoting', value: 'Remoting' },
+      ],
+      onFilter: (value, record) => record.protocol === value,
+      render: (protocol: string) => {
+        const cfg = protocolConfig[protocol] ?? { color: 'default', label: protocol };
+        return <Tag color={cfg.color}>{cfg.label}</Tag>;
+      },
+    },
+    {
+      title: t('common.address'),
+      dataIndex: 'address',
+      key: 'address',
+      width: 180,
+      render: (addr: string) => (
+        <Text style={{ fontSize: 13, fontFamily: 'monospace' }}>{addr}</Text>
+      ),
+    },
+    {
+      title: t('clients.language'),
+      dataIndex: 'language',
+      key: 'language',
+      width: 100,
+      filters: [
+        { text: 'Java', value: 'Java' },
+        { text: 'Go', value: 'Go' },
+        { text: 'Python', value: 'Python' },
+        { text: 'Rust', value: 'Rust' },
+      ],
+      onFilter: (value, record) => record.language === value,
+      render: (lang: string) => {
+        const cfg = languageConfig[lang] ?? { color: 'default', label: lang };
+        return <Tag color={cfg.color}>{cfg.label}</Tag>;
+      },
+    },
+    {
+      title: t('common.version'),
+      dataIndex: 'version',
+      key: 'version',
+      width: 90,
+    },
+    {
+      title: t('cluster.heartbeat'),
+      dataIndex: 'connectedAt',
+      key: 'connectedAt',
+      width: 170,
+      sorter: (a, b) => a.connectedAt.localeCompare(b.connectedAt),
+      render: (d: string) => (
+        <Text type="secondary" style={{ fontSize: 13 }}>
+          {d}
+        </Text>
+      ),
+    },
+  ];
+
+  /* ═══════════════════════════════════════════
+     Render
+     ═══════════════════════════════════════════ */
+  return (
+    <div style={{ padding: 24 }}>
+      {/* ─── Header ─── */}
+      <PageHeader
+        title={t('clients.title')}
+        subtitle={`${t('clients.title')} — ${filtered.length} connections`}
+      />
+
+      {/* ─── Filter Bar ─── */}
+      <Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
+        <Space size={12} wrap>
+          <Select
+            value={clusterFilter}
+            onChange={setClusterFilter}
+            style={{ width: 180 }}
+            options={clusterOptions}
+          />
+          <Input.Search
+            placeholder={t('clients.searchPlaceholder')}
+            allowClear
+            value={search}
+            onChange={(e) => setSearch(e.target.value)}
+            onSearch={setSearch}
+            style={{ width: 280 }}
+            prefix={<MagnifyingGlass size={14} color="#9CA3AF" />}
+          />
+        </Space>
+      </Flex>
+
+      {/* ─── Table ─── */}
+      <Card bodyStyle={{ padding: 0 }}>
+        <Table
+          columns={columns}
+          dataSource={filtered}
+          rowKey="clientId"
+          scroll={{ x: 1320 }}
+          pagination={{
+            pageSize: 20,
+            showSizeChanger: true,
+            showTotal: (total) => `${t('common.total')} ${total}`,
+          }}
+          size="small"
+        />
+      </Card>
+    </div>
+  );
+};
+
+export default ClientsPage;
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
new file mode 100644
index 0000000..f22ca74
--- /dev/null
+++ b/web/src/pages/cluster/index.tsx
@@ -0,0 +1,860 @@
+/*
+ * 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 { useState, useEffect } from 'react';
+import {
+  Table,
+  Tabs,
+  Tag,
+  Button,
+  Input,
+  Select,
+  Modal,
+  Form,
+  Radio,
+  Switch,
+  InputNumber,
+  Progress,
+  Flex,
+  Space,
+  Typography,
+  Card,
+  message,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import {
+  ReloadOutlined,
+  SettingOutlined,
+  EyeOutlined,
+  EditOutlined,
+  PlusOutlined,
+} from '@ant-design/icons';
+import { Cpu, HardDrives, Globe } from '@phosphor-icons/react';
+import PageHeader from '../../components/PageHeader';
+import { useLang } from '../../i18n/LangContext';
+import clusters, {
+  type BrokerInfo,
+  type ProxyInfo,
+  type NameServerInfo,
+  type ClusterConfig,
+  type ClusterInfo,
+} from '../../mock/clusters';
+
+const { Text } = Typography;
+
+// ─── Page ─────────────────────────────────────────────────────────────────────
+
+const ClusterPage = () => {
+  const { t } = useLang();
+  const [nsSearch, setNsSearch] = useState('');
+  const [brokerSearch, setBrokerSearch] = useState('');
+  const [brokerNsClusterFilter, setBrokerNsClusterFilter] = useState<string>('');
+  const [proxySearch, setProxySearch] = useState('');
+
+  const [configModalOpen, setConfigModalOpen] = useState(false);
+  const [selectedCluster, setSelectedCluster] = useState<ClusterInfo | null>(null);
+  const [nsModalOpen, setNsModalOpen] = useState(false);
+  const [nsModalMode, setNsModalMode] = useState<'create' | 'edit'>('create');
+  const [nsForm] = Form.useForm();
+  const [configForm] = Form.useForm();
+
+  // ─── Auto-refresh TPS / connections every 2s ──────────────────────────────
+  const [autoRefresh, setAutoRefresh] = useState(true);
+  // Initialize from mock base values
+  const initBrokerTps = (): Record<string, { tpsIn: number; tpsOut: number }> => {
+    const m: Record<string, { tpsIn: number; tpsOut: number }> = {};
+    clusters.forEach((c) =>
+      c.brokers.forEach((b) => {
+        m[b.addr] = { tpsIn: b.tpsIn, tpsOut: b.tpsOut };
+      }),
+    );
+    return m;
+  };
+  const initProxyConn = (): Record<string, number> => {
+    const m: Record<string, number> = {};
+    clusters.forEach((c) =>
+      c.proxies.forEach((p) => {
+        m[p.addr] = p.connections;
+      }),
+    );
+    return m;
+  };
+
+  const [brokerTpsMap, setBrokerTpsMap] = useState(initBrokerTps);
+  const [proxyConnMap, setProxyConnMap] = useState(initProxyConn);
+
+  useEffect(() => {
+    if (!autoRefresh) return;
+
+    const timer = setInterval(() => {
+      setBrokerTpsMap((prev) => {
+        const next = { ...prev };
+        clusters.forEach((c) => {
+          c.brokers.forEach((b) => {
+            const cur = next[b.addr] ?? { tpsIn: b.tpsIn, tpsOut: b.tpsOut };
+            const fluctuate = (base: number, v: number) =>
+              base === 0 ? 0 : Math.max(0, Math.round(v + (Math.random() - 0.5) * base * 0.12));
+            next[b.addr] = {
+              tpsIn: fluctuate(b.tpsIn, cur.tpsIn),
+              tpsOut: fluctuate(b.tpsOut, cur.tpsOut),
+            };
+          });
+        });
+        return next;
+      });
+
+      setProxyConnMap((prev) => {
+        const next = { ...prev };
+        clusters.forEach((c) => {
+          c.proxies.forEach((p) => {
+            const cur = prev[p.addr] ?? p.connections;
+            next[p.addr] = Math.max(
+              0,
+              Math.round(cur + (Math.random() - 0.5) * p.connections * 0.08),
+            );
+          });
+        });
+        return next;
+      });
+    }, 2000);
+
+    return () => clearInterval(timer);
+  }, [autoRefresh]);
+
+  // Broker config handler
+  const handleConfigOpen = (cluster: ClusterInfo) => {
+    const cfg: ClusterConfig = cluster.config;
+    setSelectedCluster(cluster);
+    configForm.setFieldsValue({
+      flushDiskType: cfg.flushDiskType ?? 'ASYNC_FLUSH',
+      autoCreateTopicEnable: cfg.autoCreateTopicEnable ?? false,
+      autoCreateSubscriptionGroup: cfg.autoCreateSubscriptionGroup ?? false,
+      maxMessageSizeMB: Math.round(cfg.maxMessageSize / 1048576),
+      fileReservedTime: cfg.fileReservedTime ?? 72,
+      writeQueueNums: cfg.writeQueueNums ?? 8,
+      readQueueNums: cfg.readQueueNums ?? 8,
+      brokerPermission: cfg.brokerPermission ?? 6,
+    });
+    setConfigModalOpen(true);
+  };
+
+  const tabItems = [
+    {
+      key: 'nameserver',
+      label: (
+        <Flex align="center" gap={4}>
+          <Cpu size={16} />
+          <span>{t('cluster.nameserver')}</span>
+        </Flex>
+      ),
+      children: renderNameServerTab(),
+    },
+    {
+      key: 'broker',
+      label: (
+        <Flex align="center" gap={4}>
+          <HardDrives size={16} />
+          <span>{t('cluster.broker')}</span>
+        </Flex>
+      ),
+      children: renderBrokerTab(),
+    },
+    {
+      key: 'proxy',
+      label: (
+        <Flex align="center" gap={4}>
+          <Globe size={16} />
+          <span>{t('cluster.proxy')}</span>
+        </Flex>
+      ),
+      children: renderProxyTab(),
+    },
+  ];
+
+  // ─── Tab 2: Broker 管理 (flat table) ────────────────────────────────────────
+
+  function renderBrokerTab() {
+    type BrokerWithCluster = BrokerInfo & {
+      clusterName: string;
+      nsClusterName: string;
+      cluster: ClusterInfo;
+    };
+
+    const allBrokers: BrokerWithCluster[] = clusters.flatMap((c) =>
+      c.brokers
+        .filter((b) => {
+          const matchSearch =
+            !brokerSearch ||
+            b.name.toLowerCase().includes(brokerSearch.toLowerCase()) ||
+            b.addr.toLowerCase().includes(brokerSearch.toLowerCase());
+          const matchNsCluster =
+            !brokerNsClusterFilter || c.nsClusterName === brokerNsClusterFilter;
+          return matchSearch && matchNsCluster;
+        })
+        .map((b) => {
+          const tpsOverride = brokerTpsMap[b.addr];
+          return {
+            ...b,
+            tpsIn: tpsOverride?.tpsIn ?? b.tpsIn,
+            tpsOut: tpsOverride?.tpsOut ?? b.tpsOut,
+            clusterName: c.name,
+            nsClusterName: c.nsClusterName,
+            cluster: c,
+          };
+        }),
+    );
+
+    const brokerColumns: ColumnsType<BrokerWithCluster> = [
+      {
+        title: t('cluster.k8sName'),
+        dataIndex: 'clusterName',
+        key: 'clusterName',
+        width: 160,
+        sorter: (a, b) => a.clusterName.localeCompare(b.clusterName),
+        render: (name: string) => (
+          <Text strong style={{ fontSize: 13 }}>
+            {name}
+          </Text>
+        ),
+      },
+      {
+        title: t('cluster.brokerName'),
+        dataIndex: 'name',
+        key: 'name',
+        width: 170,
+        sorter: (a, b) => a.name.localeCompare(b.name),
+        render: (name: string) => (
+          <Text strong style={{ fontSize: 13 }}>
+            {name}
+          </Text>
+        ),
+      },
+      {
+        title: t('common.status'),
+        dataIndex: 'status',
+        key: 'status',
+        width: 90,
+        sorter: (a, b) => a.status.localeCompare(b.status),
+        render: (status: string) => {
+          const map: Record<string, { color: string; label: string }> = {
+            running: { color: 'green', label: t('cluster.running') },
+            readonly: { color: 'gold', label: t('cluster.readonly') },
+            maintenance: { color: 'red', label: t('cluster.maintenance') },
+          };
+          const cfg = map[status] ?? { color: 'default', label: status };
+          return <Tag color={cfg.color}>{cfg.label}</Tag>;
+        },
+      },
+      {
+        title: t('common.version'),
+        dataIndex: 'version',
+        key: 'version',
+        width: 80,
+        align: 'right',
+        sorter: (a, b) => a.version.localeCompare(b.version),
+        render: (v: string) => <span style={{ fontSize: 13 }}>{v}</span>,
+      },
+      {
+        title: t('cluster.diskUsage'),
+        dataIndex: 'diskUsage',
+        key: 'diskUsage',
+        width: 150,
+        sorter: (a, b) => a.diskUsage - b.diskUsage,
+        render: (v: number) => (
+          <Progress
+            percent={v}
+            size="small"
+            strokeColor={v > 85 ? '#ff4d4f' : v > 70 ? '#faad14' : '#1677ff'}
+          />
+        ),
+      },
+      {
+        title: t('common.address'),
+        dataIndex: 'addr',
+        key: 'addr',
+        width: 170,
+        align: 'right',
+        sorter: (a, b) => a.addr.localeCompare(b.addr),
+        render: (addr: string) => <span style={{ fontSize: 13 }}>{addr}</span>,
+      },
+      {
+        title: 'TPS In',
+        dataIndex: 'tpsIn',
+        key: 'tpsIn',
+        width: 90,
+        align: 'right',
+        sorter: (a, b) => a.tpsIn - b.tpsIn,
+        render: (v: number) => v.toLocaleString(),
+      },
+      {
+        title: 'TPS Out',
+        dataIndex: 'tpsOut',
+        key: 'tpsOut',
+        width: 90,
+        align: 'right',
+        sorter: (a, b) => a.tpsOut - b.tpsOut,
+        render: (v: number) => v.toLocaleString(),
+      },
+      {
+        title: t('common.actions'),
+        key: 'action',
+        width: 160,
+        render: (_: unknown, record: BrokerWithCluster) => (
+          <Flex gap={6}>
+            <Button
+              size="small"
+              icon={<SettingOutlined />}
+              style={{ borderColor: '#1677ff', color: '#1677ff' }}
+              onClick={() => handleConfigOpen(record.cluster)}
+            >
+              {t('cluster.config')}
+            </Button>
+            <Button
+              size="small"
+              icon={<ReloadOutlined />}
+              danger
+              style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
+              onClick={() => message.warning(t('cluster.restartNotSupported'))}
+            >
+              {t('cluster.restart')}
+            </Button>
+          </Flex>
+        ),
+      },
+    ];
+
+    const nsClusterOptions = [
+      { value: '', label: t('common.all') },
+      ...clusters.map((c) => ({ value: c.nsClusterName, label: c.nsClusterName })),
+    ];
+
+    return (
+      <div>
+        <Flex justify="space-between" style={{ marginBottom: 16 }}>
+          <Space>
+            <Input.Search
+              placeholder={t('cluster.searchBroker')}
+              allowClear
+              onSearch={setBrokerSearch}
+              onChange={(e) => !e.target.value && setBrokerSearch('')}
+              style={{ width: 240 }}
+            />
+            <Select
+              value={brokerNsClusterFilter}
+              onChange={setBrokerNsClusterFilter}
+              style={{ width: 180 }}
+              options={nsClusterOptions}
+            />
+          </Space>
+          <Button
+            type="primary"
+            icon={<PlusOutlined />}
+            onClick={() => message.info('新建集群功能开发中')}
+          >
+            {t('cluster.createCluster')}
+          </Button>
+        </Flex>
+        <Card bodyStyle={{ padding: 0 }}>
+          <Table
+            columns={brokerColumns}
+            dataSource={allBrokers}
+            rowKey="addr"
+            pagination={{ pageSize: 20 }}
+            size="small"
+          />
+        </Card>
+
+        {selectedCluster && (
+          <Modal
+            title={`配置 - ${selectedCluster.name}`}
+            open={configModalOpen}
+            onCancel={() => setConfigModalOpen(false)}
+            onOk={() => {
+              configForm.validateFields().then(() => {
+                message.success('配置已更新');
+                setConfigModalOpen(false);
+              });
+            }}
+            width={560}
+          >
+            <Form form={configForm} layout="vertical">
+              <Form.Item label="刷盘方式" name="flushDiskType">
+                <Radio.Group>
+                  <Radio value="SYNC_FLUSH">同步刷盘</Radio>
+                  <Radio value="ASYNC_FLUSH">异步刷盘</Radio>
+                </Radio.Group>
+              </Form.Item>
+              <Form.Item
+                label="自动创建 Topic"
+                name="autoCreateTopicEnable"
+                valuePropName="checked"
+              >
+                <Switch />
+              </Form.Item>
+              <Form.Item
+                label="自动创建订阅组"
+                name="autoCreateSubscriptionGroup"
+                valuePropName="checked"
+              >
+                <Switch />
+              </Form.Item>
+              <Form.Item label="最大消息大小 (MB)" name="maxMessageSizeMB">
+                <InputNumber min={1} max={128} style={{ width: '100%' }} />
+              </Form.Item>
+              <Form.Item label="文件保留时长 (小时)" name="fileReservedTime">
+                <InputNumber min={1} max={720} style={{ width: '100%' }} />
+              </Form.Item>
+              <Form.Item label="写队列数" name="writeQueueNums">
+                <InputNumber min={1} max={256} style={{ width: '100%' }} />
+              </Form.Item>
+              <Form.Item label="读队列数" name="readQueueNums">
+                <InputNumber min={1} max={256} style={{ width: '100%' }} />
+              </Form.Item>
+              <Form.Item label="Broker 权限" name="brokerPermission">
+                <InputNumber min={0} max={7} style={{ width: '100%' }} />
+              </Form.Item>
+            </Form>
+          </Modal>
+        )}
+      </div>
+    );
+  }
+
+  // ─── Tab 2: NameServer 管理 (nested by cluster) ────────────────────────────
+
+  function renderNameServerTab() {
+    const filteredClusters = clusters
+      .map((c) => {
+        const nameServers = c.nameServers.filter((ns) => {
+          const matchSearch = !nsSearch || ns.addr.toLowerCase().includes(nsSearch.toLowerCase());
+          return matchSearch;
+        });
+        return { ...c, filteredNameServers: nameServers };
+      })
+      .filter((c) => c.filteredNameServers.length > 0);
+
+    const getNsSubColumns = (clusterId: string): ColumnsType<NameServerInfo> => [
+      {
+        title: t('common.address'),
+        dataIndex: 'addr',
+        key: 'addr',
+        sorter: (a, b) => a.addr.localeCompare(b.addr),
+        render: (addr: string) => (
+          <Text code style={{ fontSize: 12 }}>
+            {addr}
+          </Text>
+        ),
+      },
+      {
+        title: t('common.status'),
+        dataIndex: 'status',
+        key: 'status',
+        width: 90,
+        sorter: (a, b) => a.status.localeCompare(b.status),
+        render: (status: string) => {
+          const map: Record<string, { color: string; label: string }> = {
+            healthy: { color: 'green', label: t('cluster.running') },
+            warning: { color: 'gold', label: '告警' },
+            error: { color: 'red', label: '异常' },
+            offline: { color: 'default', label: '离线' },
+          };
+          const cfg = map[status] ?? { color: 'default', label: status };
+          return <Tag color={cfg.color}>{cfg.label}</Tag>;
+        },
+      },
+      {
+        title: t('common.actions'),
+        key: 'action',
+        width: 100,
+        render: (_: unknown, record: NameServerInfo) => (
+          <Flex gap={6}>
+            <Button
+              size="small"
+              icon={<EditOutlined />}
+              style={{ borderColor: '#722ed1', color: '#722ed1' }}
+              onClick={() => {
+                setNsModalMode('edit');
+                nsForm.setFieldsValue({ clusterId, addr: record.addr, newAddr: '' });
+                setNsModalOpen(true);
+              }}
+            >
+              {t('common.edit')}
+            </Button>
+          </Flex>
+        ),
+      },
+    ];
+
+    const clusterColumns: ColumnsType<ClusterInfo & { filteredNameServers: NameServerInfo[] }> = [
+      {
+        title: t('cluster.k8sName'),
+        dataIndex: 'name',
+        key: 'name',
+        width: 180,
+        render: (name: string) => (
+          <Text strong style={{ fontSize: 14 }}>
+            {name}
+          </Text>
+        ),
+      },
+      {
+        title: t('cluster.nsClusterName'),
+        dataIndex: 'nsClusterName',
+        key: 'nsClusterName',
+        width: 180,
+        render: (name: string) => (
+          <Text strong style={{ fontSize: 13 }}>
+            {name}
+          </Text>
+        ),
+      },
+      {
+        title: t('common.status'),
+        dataIndex: 'status',
+        key: 'status',
+        width: 90,
+        render: (status: string) => {
+          const map: Record<string, { color: string; label: string }> = {
+            healthy: { color: 'green', label: t('cluster.running') },
+            warning: { color: 'gold', label: '告警' },
+            error: { color: 'red', label: '异常' },
+            offline: { color: 'default', label: '离线' },
+          };
+          const cfg = map[status] ?? { color: 'default', label: status };
+          return <Tag color={cfg.color}>{cfg.label}</Tag>;
+        },
+      },
+      {
+        title: t('common.version'),
+        dataIndex: 'version',
+        key: 'version',
+        width: 80,
+        render: (v: string) => <Tag>{v}</Tag>,
+      },
+      {
+        title: t('cluster.count'),
+        key: 'nsCount',
+        width: 80,
+        align: 'center',
+        render: (_: unknown, record: ClusterInfo & { filteredNameServers: NameServerInfo[] }) =>
+          record.filteredNameServers.length,
+      },
+    ];
+
+    return (
+      <div>
+        <Flex justify="space-between" style={{ marginBottom: 16 }}>
+          <Space>
+            <Input.Search
+              placeholder={t('cluster.searchNs')}
+              allowClear
+              onSearch={setNsSearch}
+              onChange={(e) => !e.target.value && setNsSearch('')}
+              style={{ width: 240 }}
+            />
+          </Space>
+          <Button
+            type="primary"
+            icon={<PlusOutlined />}
+            onClick={() => {
+              setNsModalMode('create');
+              nsForm.resetFields();
+              setNsModalOpen(true);
+            }}
+          >
+            {t('cluster.createNameServer')}
+          </Button>
+        </Flex>
+        <Card bodyStyle={{ padding: 0 }}>
+          <Table
+            columns={clusterColumns}
+            dataSource={filteredClusters}
+            rowKey="id"
+            pagination={{ pageSize: 20 }}
+            size="small"
+            expandable={{
+              expandedRowRender: (record) => (
+                <div style={{ padding: '8px 0' }}>
+                  <Table
+                    columns={getNsSubColumns(record.id)}
+                    dataSource={record.filteredNameServers}
+                    rowKey="addr"
+                    pagination={false}
+                    size="small"
+                  />
+                </div>
+              ),
+            }}
+          />
+        </Card>
+      </div>
+    );
+  }
+
+  // ─── Tab 3: Proxy 管理 (flat table) ────────────────────────────────────────
+
+  function renderProxyTab() {
+    type ProxyRow = ProxyInfo & { clusterName: string; nsClusterName: string };
+
+    const allProxies: ProxyRow[] = clusters
+      .filter((c) => c.proxies.length > 0)
+      .flatMap((c) =>
+        c.proxies
+          .filter((p) => {
+            const matchSearch =
+              !proxySearch || p.addr.toLowerCase().includes(proxySearch.toLowerCase());
+            return matchSearch;
+          })
+          .map((p) => ({
+            ...p,
+            connections: proxyConnMap[p.addr] ?? p.connections,
+            clusterName: c.name,
+            nsClusterName: c.nsClusterName,
+          })),
+      );
+
+    const proxyColumns: ColumnsType<ProxyRow> = [
+      {
+        title: t('cluster.k8sName'),
+        dataIndex: 'clusterName',
+        key: 'clusterName',
+        width: 160,
+        sorter: (a, b) => a.clusterName.localeCompare(b.clusterName),
+        render: (name: string) => (
+          <Text strong style={{ fontSize: 13 }}>
+            {name}
+          </Text>
+        ),
+      },
+      {
+        title: t('cluster.proxyAddr'),
+        dataIndex: 'addr',
+        key: 'addr',
+        width: 200,
+        sorter: (a, b) => a.addr.localeCompare(b.addr),
+        render: (addr: string) => (
+          <Text code style={{ fontSize: 12 }}>
+            {addr}
+          </Text>
+        ),
+      },
+      {
+        title: t('common.status'),
+        dataIndex: 'status',
+        key: 'status',
+        width: 90,
+        sorter: (a, b) => a.status.localeCompare(b.status),
+        render: (status: string) => {
+          const map: Record<string, { color: string; label: string }> = {
+            healthy: { color: 'green', label: t('cluster.running') },
+            warning: { color: 'gold', label: '告警' },
+            error: { color: 'red', label: '异常' },
+            offline: { color: 'default', label: '离线' },
+          };
+          const cfg = map[status] ?? { color: 'default', label: status };
+          return <Tag color={cfg.color}>{cfg.label}</Tag>;
+        },
+      },
+      {
+        title: t('cluster.connections'),
+        dataIndex: 'connections',
+        key: 'connections',
+        width: 100,
+        align: 'right',
+        sorter: (a, b) => a.connections - b.connections,
+        render: (v: number) => v.toLocaleString(),
+      },
+      {
+        title: t('cluster.grpcPort'),
+        dataIndex: 'grpcPort',
+        key: 'grpcPort',
+        width: 100,
+        align: 'center',
+        sorter: (a, b) => a.grpcPort - b.grpcPort,
+      },
+      {
+        title: t('cluster.remotingPort'),
+        dataIndex: 'remotingPort',
+        key: 'remotingPort',
+        width: 120,
+        align: 'center',
+        sorter: (a, b) => a.remotingPort - b.remotingPort,
+      },
+      {
+        title: t('common.actions'),
+        key: 'action',
+        width: 160,
+        render: (_: unknown, record: ProxyRow) => (
+          <Flex gap={6}>
+            <Button
+              size="small"
+              icon={<EyeOutlined />}
+              style={{ borderColor: '#1677ff', color: '#1677ff' }}
+              onClick={() => message.info(`查看详情: ${record.addr}`)}
+            >
+              {t('common.detail')}
+            </Button>
+            <Button
+              size="small"
+              icon={<ReloadOutlined />}
+              style={{ borderColor: '#faad14', color: '#faad14' }}
+              onClick={() => {
+                Modal.confirm({
+                  title: t('cluster.confirmRestart'),
+                  content: `确定要重启 Proxy "${record.addr}" 吗?`,
+                  okText: '确认',
+                  cancelText: '取消',
+                  onOk: () => message.success(`Proxy 重启已提交: ${record.addr}`),
+                });
+              }}
+            >
+              {t('cluster.restart')}
+            </Button>
+          </Flex>
+        ),
+      },
+    ];
+
+    return (
+      <div>
+        <Flex justify="space-between" style={{ marginBottom: 16 }}>
+          <Space>
+            <Input.Search
+              placeholder={t('cluster.searchProxy')}
+              allowClear
+              onSearch={setProxySearch}
+              onChange={(e) => !e.target.value && setProxySearch('')}
+              style={{ width: 240 }}
+            />
+          </Space>
+          <Button
+            type="primary"
+            icon={<PlusOutlined />}
+            onClick={() => message.info('新建集群功能开发中')}
+          >
+            {t('cluster.createCluster')}
+          </Button>
+        </Flex>
+        <Card bodyStyle={{ padding: 0 }}>
+          <Table
+            columns={proxyColumns}
+            dataSource={allProxies}
+            rowKey={(r) => `${r.clusterName}-${r.addr}`}
+            pagination={{ pageSize: 20 }}
+            size="small"
+          />
+        </Card>
+      </div>
+    );
+  }
+
+  // ─── Tab 4: K8s 证书管理 ───────────────────────────────────────────────────
+
+  // ─── Render ─────────────────────────────────────────────────────────────────
+
+  const totalBrokers = clusters.reduce((s, c) => s + c.brokers.length, 0);
+  const totalNameServers = clusters.reduce((s, c) => s + c.nameServers.length, 0);
+  const totalProxies = clusters.reduce((s, c) => s + c.proxies.length, 0);
+
+  return (
+    <div style={{ padding: 24 }}>
+      <PageHeader
+        title={t('cluster.title')}
+        subtitle={`${t('common.total')} ${clusters.length} ${t('cluster.title')} · ${totalBrokers} Broker · ${totalNameServers} NameServer · ${totalProxies} Proxy`}
+        extra={
+          <Flex align="center" gap={6}>
+            {autoRefresh && (
+              <span
+                style={{
+                  width: 6,
+                  height: 6,
+                  borderRadius: '50%',
+                  background: '#52c41a',
+                  display: 'inline-block',
+                  animation: 'livePulse 1.5s ease-in-out infinite',
+                }}
+              />
+            )}
+            <Text type="secondary" style={{ fontSize: 12 }}>
+              {autoRefresh ? t('common.liveRefresh') : t('common.autoRefresh')}
+            </Text>
+            <Switch size="small" checked={autoRefresh} onChange={setAutoRefresh} />
+          </Flex>
+        }
+      />
+      <style>{`
+        @keyframes livePulse {
+          0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(82, 196, 26, 0.4); }
+          50% { opacity: 0.6; box-shadow: 0 0 0 4px rgba(82, 196, 26, 0); }
+        }
+      `}</style>
+      {/* ─── NameServer Create/Edit Modal ─── */}
+      <Modal
+        title={
+          nsModalMode === 'create' ? t('cluster.createNameServer') : t('cluster.editNameServer')
+        }
+        open={nsModalOpen}
+        onCancel={() => setNsModalOpen(false)}
+        onOk={() => {
+          nsForm.validateFields().then((values: Record<string, string>) => {
+            if (nsModalMode === 'create') {
+              message.success(`${t('cluster.nsCreated')}: ${values.addr}`);
+            } else {
+              message.success(
+                `${t('cluster.nsUpdated')}: ${values.addr}${values.newAddr ? ` → ${values.newAddr}` : ''}`,
+              );
+            }
+            setNsModalOpen(false);
+          });
+        }}
+        okText={t('common.confirm')}
+        cancelText={t('common.cancel')}
+        destroyOnClose
+      >
+        <Form form={nsForm} layout="vertical" style={{ marginTop: 16 }}>
+          {nsModalMode === 'create' && (
+            <Form.Item
+              name="clusterId"
+              label={t('cluster.selectCluster')}
+              rules={[{ required: true, message: t('cluster.selectCluster') }]}
+            >
+              <Select
+                placeholder={t('cluster.selectCluster')}
+                options={clusters.map((c) => ({ label: c.name, value: c.id }))}
+              />
+            </Form.Item>
+          )}
+          <Form.Item
+            name="addr"
+            label={t('cluster.nsAddr')}
+            rules={[{ required: true, message: t('cluster.nsAddr') }]}
+          >
+            <Input placeholder={t('cluster.nsAddrPlaceholder')} disabled={nsModalMode === 'edit'} />
+          </Form.Item>
+          {nsModalMode === 'edit' && (
+            <Form.Item name="newAddr" label={t('cluster.newAddr')}>
+              <Input placeholder={t('cluster.newAddrPlaceholder')} />
+            </Form.Item>
+          )}
+        </Form>
+      </Modal>
+      <Tabs items={tabItems} defaultActiveKey="broker" />
+    </div>
+  );
+};
+
+export default ClusterPage;
diff --git a/web/src/pages/home/dashboard.tsx b/web/src/pages/home/dashboard.tsx
new file mode 100644
index 0000000..b1aa484
--- /dev/null
+++ b/web/src/pages/home/dashboard.tsx
@@ -0,0 +1,207 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Row, Col, Card, Statistic, Table, Tag, Typography } from 'antd';
+import { ClusterOutlined, ThunderboltOutlined } from '@ant-design/icons';
+import { ListDashes, ArrowDown } from '@phosphor-icons/react';
+import PageHeader from '../../components/PageHeader';
+import StatusBadge from '../../components/StatusBadge';
+import MiniBar from '../../components/MiniBar';
+import { CLUSTER_TYPE_MAP } from '../../constants/theme';
+import { getDashboard } from '../../services/dashboardService';
+import type { DashboardData } from '../../api/metrics';
+import { useLang } from '../../i18n/LangContext';
+
+const { Text } = Typography;
+
+const DashboardPage = () => {
+  const navigate = useNavigate();
+  const { t } = useLang();
+  const [dashboard, setDashboard] = useState<DashboardData | null>(null);
+
+  useEffect(() => {
+    getDashboard().then(setDashboard).catch(console.error);
+  }, []);
+
+  if (!dashboard) return null;
+
+  const { stats, clusters } = dashboard;
+
+  const statCards = [
+    {
+      title: t('dashboard.clusters'),
+      value: stats.totalClusters,
+      icon: <ClusterOutlined style={{ fontSize: 22, color: '#52c41a' }} />,
+      color: '#52c41a',
+      suffix: '',
+      detail: `${stats.totalBrokers} Brokers · ${stats.totalProxies} Proxy`,
+    },
+    {
+      title: t('dashboard.topics'),
+      value: stats.totalTopics,
+      icon: <ListDashes size={22} weight="duotone" color="#1677ff" />,
+      color: '#1677ff',
+      suffix: '',
+      detail: `${stats.totalConsumerGroups} Consumer Groups`,
+    },
+    {
+      title: t('dashboard.tpsIn'),
+      value: stats.tpsIn,
+      icon: <ArrowDown size={22} weight="duotone" color="#fa8c16" />,
+      color: '#fa8c16',
+      suffix: '/s',
+      detail: `${t('dashboard.tpsOut')}: ${stats.tpsOut.toLocaleString()}/s`,
+    },
+    {
+      title: t('dashboard.todayMessages'),
+      value: stats.totalMessagesToday,
+      icon: <ThunderboltOutlined style={{ fontSize: 22, color: '#722ed1' }} />,
+      color: '#722ed1',
+      suffix: '',
+      detail: t('dashboard.millionMessages', {
+        n: (stats.totalMessagesToday / 1_000_000).toFixed(1),
+      }),
+    },
+  ];
+
+  const clusterColumns = [
+    {
+      title: t('dashboard.clusterName'),
+      dataIndex: 'name',
+      key: 'name',
+      render: (name: string) => <Text strong>{name}</Text>,
+    },
+    {
+      title: t('common.status'),
+      dataIndex: 'status',
+      key: 'status',
+      width: 100,
+      render: (status: string) => <StatusBadge status={status} />,
+    },
+    {
+      title: t('common.type'),
+      dataIndex: 'type',
+      key: 'type',
+      render: (type: string) => {
+        const info = CLUSTER_TYPE_MAP[type];
+        return info ? <Tag color={info.color}>{info.label}</Tag> : type;
+      },
+    },
+    {
+      title: t('common.version'),
+      dataIndex: 'version',
+      key: 'version',
+      render: (v: string) => <span style={{ fontSize: 13 }}>{v}</span>,
+    },
+    {
+      title: t('dashboard.broker'),
+      dataIndex: 'brokers',
+      key: 'brokers',
+      width: 80,
+      align: 'center' as const,
+      render: (v: number) => Math.max(0, v),
+    },
+    {
+      title: t('dashboard.proxy'),
+      dataIndex: 'proxies',
+      key: 'proxies',
+      width: 80,
+      align: 'center' as const,
+      render: (v: number) => Math.max(0, v),
+    },
+    {
+      title: t('dashboard.topic'),
+      dataIndex: 'topics',
+      key: 'topics',
+      width: 80,
+      align: 'center' as const,
+    },
+    {
+      title: t('dashboard.group'),
+      dataIndex: 'groups',
+      key: 'groups',
+      width: 80,
+      align: 'center' as const,
+    },
+    {
+      title: t('dashboard.tpsIn'),
+      dataIndex: 'tpsIn',
+      key: 'tpsIn',
+      width: 90,
+      align: 'right' as const,
+      render: (v: number) => v.toLocaleString(),
+    },
+    {
+      title: t('dashboard.tpsOut'),
+      dataIndex: 'tpsOut',
+      key: 'tpsOut',
+      width: 90,
+      align: 'right' as const,
+      render: (v: number) => v.toLocaleString(),
+    },
+    {
+      title: t('dashboard.trend'),
+      dataIndex: 'throughput',
+      key: 'throughput',
+      width: 110,
+      render: (data: number[], record: DashboardData['clusters'][0]) => (
+        <MiniBar
+          data={data}
+          color={
+            record.status === 'healthy'
+              ? '#52c41a'
+              : record.status === 'warning'
+                ? '#faad14'
+                : '#d9d9d9'
+          }
+          height={26}
+          width={100}
+        />
+      ),
+    },
+  ];
+
+  return (
+    <div style={{ padding: 24 }}>
+      <PageHeader title={t('dashboard.title')} subtitle={t('dashboard.subtitle')} />
+
+      <Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
+        {statCards.map((card) => (
+          <Col xs={12} sm={12} md={6} key={card.title}>
+            <Card size="small" style={{ borderTop: `3px solid ${card.color}`, borderRadius: 8 }}>
+              <Statistic
+                title={card.title}
+                value={card.value}
+                prefix={card.icon}
+                suffix={card.suffix}
+                valueStyle={{ fontSize: 28, fontWeight: 600 }}
+              />
+              <Text type="secondary" style={{ fontSize: 12, marginTop: 4, display: 'block' }}>
+                {card.detail}
+              </Text>
+            </Card>
+          </Col>
+        ))}
+      </Row>
+
+      <Card
+        title={t('dashboard.clusterHealth')}
+        extra={<a onClick={() => navigate('/cluster')}>{t('common.viewAll')}</a>}
+        styles={{ body: { padding: '0 20px 16px' } }}
+      >
+        <Table
+          dataSource={clusters}
+          columns={clusterColumns}
+          rowKey="id"
+          size="small"
+          pagination={false}
+          onRow={() => ({
+            style: { cursor: 'pointer' },
+            onClick: () => navigate('/cluster'),
+          })}
+        />
+      </Card>
+    </div>
+  );
+};
+
+export default DashboardPage;
diff --git a/web/src/pages/home/index.tsx b/web/src/pages/home/index.tsx
new file mode 100644
index 0000000..dcd9d2a
--- /dev/null
+++ b/web/src/pages/home/index.tsx
@@ -0,0 +1,415 @@
+/*
+ * 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 { useState, useRef, useEffect, useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { RobotOutlined } from '@ant-design/icons';
+import { Select, ConfigProvider, theme } from 'antd';
+import {
+  Stethoscope,
+  ChatCircleDots,
+  ClockCounterClockwise,
+  SlidersHorizontal,
+  Sparkle,
+  Microphone,
+  ArrowUp,
+  MagnifyingGlass,
+  CaretDown,
+  MegaphoneSimple,
+  Database,
+} from '@phosphor-icons/react';
+import { useLang } from '../../i18n/LangContext';
+
+/* ─── Time-aware greeting key ─── */
+function getGreetingKey(): string {
+  const h = new Date().getHours();
+  if (h < 6) return 'home.greeting.night';
+  if (h < 12) return 'home.greeting.morning';
+  if (h < 14) return 'home.greeting.noon';
+  if (h < 18) return 'home.greeting.afternoon';
+  return 'home.greeting.evening';
+}
+
+/* ─── Mode definitions (keys only, labels resolved via t()) ─── */
+const modes = [
+  { key: 'query', labelKey: 'home.mode.query', icon: MagnifyingGlass },
+  { key: 'diagnose', labelKey: 'home.mode.diagnose', icon: Stethoscope },
+  { key: 'manage', labelKey: 'home.mode.manage', icon: Database },
+  { key: 'chat', labelKey: 'home.mode.chat', icon: ChatCircleDots },
+];
+
+/* ─── Model option values ─── */
+const modelOptions = [
+  { value: 'qwen3.7-max', recommended: true },
+  { value: 'qwen3.7-plus', recommended: false },
+  { value: 'claude-opus-4.7', recommended: false },
+  { value: 'gpt-5.4', recommended: false },
+];
+
+/* ═══════════════════════════════════════════════════════
+   HomePage Component
+   ═══════════════════════════════════════════════════════ */
+const HomePage = () => {
+  const [activeMode, setActiveMode] = useState('query');
+  const [selectedModel, setSelectedModel] = useState('qwen3.7-max');
+  const [indicatorStyle, setIndicatorStyle] = useState({ width: 83, left: 6 });
+  const textareaRef = useRef<HTMLTextAreaElement>(null);
+  const modeBarRef = useRef<HTMLDivElement>(null);
+  const navigate = useNavigate();
+  const { t, lang } = useLang();
+
+  /* ─── Mode switch handler ─── */
+  const handleModeSwitch = useCallback((key: string, btn: HTMLButtonElement) => {
+    setActiveMode(key);
+    const parent = modeBarRef.current;
+    if (parent) {
+      const rect = btn.getBoundingClientRect();
+      const parentRect = parent.getBoundingClientRect();
+      setIndicatorStyle({
+        width: rect.width,
+        left: rect.left - parentRect.left - 6,
+      });
+    }
+  }, []);
+
+  /* ─── Fix indicator on mount and language/mode change ─── */
+  useEffect(() => {
+    const parent = modeBarRef.current;
+    if (!parent) return;
+    const activeBtn = parent.querySelector('.mode-btn.active') as HTMLButtonElement | null;
+    if (activeBtn) {
+      const rect = activeBtn.getBoundingClientRect();
+      const parentRect = parent.getBoundingClientRect();
+      setIndicatorStyle({
+        width: rect.width,
+        left: rect.left - parentRect.left - 6,
+      });
+    }
+  }, [lang, activeMode]);
+
+  /* ─── Auto-resize textarea ─── */
+  useEffect(() => {
+    const ta = textareaRef.current;
+    if (!ta) return;
+    const handler = () => {
+      ta.style.height = 'auto';
+      ta.style.height = `${Math.min(ta.scrollHeight, 300)}px`;
+    };
+    ta.addEventListener('input', handler);
+    return () => ta.removeEventListener('input', handler);
+  }, []);
+
+  /* ─── Keyboard shortcut: Enter to send ─── */
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    if (e.key === 'Enter' && !e.shiftKey) {
+      e.preventDefault();
+      navigate('/ai');
+    }
+  };
+
+  return (
+    <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
+      <div
+        className="relative w-full overflow-y-auto scrollbar-hide"
+        style={{
+          minHeight: 'calc(100vh - 48px)',
+          display: 'flex',
+          flexDirection: 'column',
+          background: '#fff',
+        }}
+      >
+        {/* ── Animated Orbs Background ── */}
+        <div
+          className="pointer-events-none absolute inset-0 overflow-hidden"
+          style={{ animation: '8s ease-in-out infinite oneday-bg-drift' }}
+        >
+          <div
+            aria-hidden="true"
+            className="absolute"
+            style={{
+              top: '-14%',
+              left: '-7%',
+              width: '42%',
+              height: '42%',
+              background:
+                'radial-gradient(circle at 30% 30%, rgb(186, 230, 253) 0%, transparent 65%)',
+              opacity: 0.45,
+              filter: 'blur(80px)',
+              animation: '8s ease-in-out infinite oneday-orb-drift-a',
+              willChange: 'transform, opacity',
+            }}
+          />
+          <div
+            aria-hidden="true"
+            className="absolute inset-0"
+            style={{
+              backgroundImage: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='1'/></svg>")`,
+              opacity: 0.025,
+              mixBlendMode: 'multiply',
+            }}
+          />
+        </div>
+
+        {/* ── Top Banner ── */}
+        <div className="sticky top-0 z-10 w-full">
+          <div
+            className="flex justify-center items-center backdrop-blur-[8px]"
+            style={{
+              background: 'linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, transparent 100%)',
+            }}
+          >
+            <div className="max-w-[920px] w-full mx-auto">
+              <div className="flex justify-center items-center px-4 py-2 min-h-[36px]">
+                <span className="inline-flex items-center gap-2 text-sm text-amber-600 cursor-pointer hover:text-amber-700 transition-colors">
+                  <MegaphoneSimple size={16} weight="fill" />
+                  <span>RocketMQ Studio — 跨集群 · 跨架构 · 跨云的统一管控平台</span>
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        {/* ── Main Content ── */}
+        <div
+          className="relative z-[1] mx-auto flex w-full max-w-[960px] flex-col items-center px-6"
+          style={{ flex: 1, justifyContent: 'center' }}
+        >
+          {/* ── Greeting Section ── */}
+          <div
+            className="pt-4"
+            style={{
+              marginBottom: 32,
+              textAlign: 'center',
+              animation: 'float-in 0.6s ease-out',
+            }}
+          >
+            <div
+              style={{
+                fontSize: '3rem',
+                lineHeight: 1.15,
+                fontWeight: 600,
+                letterSpacing: '-0.025em',
+                whiteSpace: 'nowrap',
+                color: 'var(--bolt-elements-textPrimary)',
+              }}
+            >
+              {t(getGreetingKey())}
+              {lang === 'zh' ? ',欢迎' : ', welcome'}
+              <span
+                style={{
+                  display: 'inline-block',
+                  transformOrigin: '70% 70%',
+                  animation: 'wave 1.6s ease-in-out infinite',
+                  margin: '0 0.2em',
+                }}
+              >
+                🚀
+              </span>
+              {lang === 'zh' ? t('home.welcomeTo') : 'to'}{' '}
+              <span
+                className="bg-clip-text text-transparent bg-gradient-to-r from-violet-600 via-fuchsia-500 to-orange-500"
+                style={{ marginLeft: '0.15em' }}
+              >
+                RocketMQ Studio
+              </span>
+            </div>
+            <div
+              style={{
+                marginTop: 12,
+                fontSize: '1.071rem',
+                lineHeight: 1.5,
+                color: 'var(--bolt-elements-textSecondary)',
+              }}
+            >
+              {t('home.tagline')}
+            </div>
+          </div>
+
+          {/* ── Chat Input Area ── */}
+          <div className="w-full" style={{ animation: 'float-in 0.6s ease-out 0.1s both' }}>
+            <div className="w-full mx-auto relative z-[300] max-w-[60rem]">
+              {/* Mode Toggle Bar */}
+              <div className="flex justify-center mb-5">
+                <div
+                  ref={modeBarRef}
+                  className="relative rounded-full bg-white/70 shadow-[0_8px_24px_rgba(120,120,180,0.08)] backdrop-blur-md border border-white"
+                  style={{
+                    display: 'inline-flex',
+                    alignItems: 'center',
+                    gap: 4,
+                    padding: 6,
+                  }}
+                >
+                  {/* Active indicator */}
+                  <div
+                    className="pointer-events-none absolute top-1.5 bottom-1.5 rounded-full bg-purple-50 shadow-sm transition-all duration-300"
+                    style={{
+                      width: indicatorStyle.width,
+                      transform: `translateX(${indicatorStyle.left}px)`,
+                    }}
+                  />
+
+                  {modes.map((m) => {
+                    const Icon = m.icon;
+                    return (
+                      <button
+                        key={m.key}
+                        className={`mode-btn relative ${activeMode === m.key ? 'active' : ''}`}
+                        onClick={(e) => handleModeSwitch(m.key, e.currentTarget)}
+                      >
+                        <Icon size={16} weight={activeMode === m.key ? 'fill' : 'regular'} />
+                        <span>{t(m.labelKey)}</span>
+                      </button>
+                    );
+                  })}
+                </div>
+              </div>
+
+              {/* Main Input Box */}
+              <div className="relative overflow-visible border-[1.5px] backdrop-blur-xl border-white mx-auto rounded-2xl bg-white/80 shadow-[0_20px_60px_-20px_rgba(80,90,180,0.18)]">
+                {/* Model Selector & History */}
+                <div className="flex items-center justify-between gap-3 px-3.5 pt-4">
+                  <div className="flex flex-1 min-w-0 items-center gap-2">
+                    <Select
+                      size="small"
+                      value={selectedModel}
+                      onChange={(val) => setSelectedModel(val)}
+                      options={modelOptions.map((m) => ({
+                        value: m.value,
+                        label: m.recommended ? (
+                          <span className="inline-flex items-center gap-1.5">
+                            {m.value}
+                            <span className="px-1 py-0.5 rounded text-[0.625rem] leading-none bg-purple-50 text-purple-600 font-medium">
+                              {lang === 'zh' ? '推荐' : 'Rec.'}
+                            </span>
+                          </span>
+                        ) : (
+                          m.value
+                        ),
+                      }))}
+                      variant="borderless"
+                      popupMatchSelectWidth={false}
+                      suffixIcon={<CaretDown size={10} color="#9CA3AF" />}
+                      className="model-selector"
+                      style={{ fontSize: '0.893rem' }}
+                    />
+                  </div>
+                  <div className="flex shrink-0 items-center gap-1">
+                    <button className="p-1 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors">
+                      <ClockCounterClockwise size={20} />
+                    </button>
+                  </div>
+                </div>
+
+                {/* Textarea */}
+                <div className="relative flex flex-col">
+                  <textarea
+                    ref={textareaRef}
+                    className="chat-input"
+                    placeholder="向 RocketMQ Bot 提问,全程加密、安全、可信"
+                    onKeyDown={handleKeyDown}
+                  />
+                  <RobotOutlined
+                    className="text-gray-400"
+                    style={{
+                      position: 'absolute',
+                      top: 18,
+                      left: 26,
+                      fontSize: 17,
+                    }}
+                  />
+                </div>
+
+                {/* Bottom Toolbar */}
+                <div className="flex justify-between text-sm items-center px-3.5 py-3 border-t border-gray-100/80">
+                  <div className="flex flex-1 gap-1 items-center min-w-0">
+                    <div className="flex items-center gap-2 w-full">
+                      <div className="flex-1 min-w-0">
+                        <div className="flex items-center gap-2 overflow-x-auto scrollbar-hide max-w-full py-2">
+                          <button className="tool-btn">
+                            <SlidersHorizontal size={17} />
+                            <span>工具</span>
+                          </button>
+                          <button className="tool-btn">
+                            <Sparkle size={17} />
+                            <span>Prompt 增强</span>
+                          </button>
+                          <button
+                            className="tool-btn"
+                            style={{ minHeight: 30, minWidth: 32, padding: 6 }}
+                          >
+                            <Microphone size={17} />
+                          </button>
+                        </div>
+                      </div>
+                      <div className="shrink-0 flex items-center gap-1">
+                        <button
+                          className="flex items-center justify-center w-9 h-9 rounded-full bg-gradient-to-r from-purple-500 to-violet-600 text-white shadow-lg hover:shadow-xl transition-all hover:scale-105"
+                          onClick={() => navigate('/ai')}
+                        >
+                          <ArrowUp size={19} weight="bold" />
+                        </button>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        {/* ── Footer ── */}
+        <footer
+          className="pointer-events-none text-gray-400"
+          style={{
+            position: 'relative',
+            zIndex: 1,
+            paddingBottom: 24,
+            paddingTop: 64,
+            textAlign: 'center',
+            fontSize: '0.857rem',
+            lineHeight: 1.5,
+            background:
+              'linear-gradient(to top, var(--bolt-elements-bg-depth-1) 0%, color-mix(in srgb, var(--bolt-elements-bg-depth-1) 92%, transparent) 55%, transparent 100%)',
+          }}
+        >
+          <span className="pointer-events-auto">
+            <a
+              href="#"
+              className="transition-colors hover:text-purple-500"
+              style={{ textDecoration: 'none' }}
+            >
+              文档中心
+            </a>
+            <span style={{ margin: '0 4px' }}>|</span>
+            <a
+              href="#"
+              className="transition-colors hover:text-purple-500"
+              style={{ textDecoration: 'none' }}
+            >
+              RocketMQ 社区
+            </a>
+            <span style={{ margin: '0 4px' }}>|</span>
+            <span>RocketMQ Studio 出品</span>
+          </span>
+        </footer>
+      </div>
+    </ConfigProvider>
+  );
+};
+
+export default HomePage;
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
new file mode 100644
index 0000000..a663ecd
--- /dev/null
+++ b/web/src/pages/instance/acl.tsx
@@ -0,0 +1,754 @@
+/*
+ * 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 { useState } from 'react';
+import {
+  Table,
+  Card,
+  Button,
+  Tag,
+  Space,
+  Input,
+  Select,
+  Tabs,
+  Modal,
+  Form,
+  Switch,
+  Checkbox,
+  Radio,
+  Badge,
+  Typography,
+  Flex,
+  message,
+} from 'antd';
+import { Plus, MagnifyingGlass, ShieldCheck, User, Eye, EyeSlash } from '@phosphor-icons/react';
+import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
+import type { ColumnsType } from 'antd/es/table';
+import PageHeader from '../../components/PageHeader';
+import { useLang } from '../../i18n/LangContext';
+import { aclRules, aclUsers } from '../../mock/acl';
+import type { AclRule, AclUser } from '../../mock/acl';
+
+/* ═══════════════════════════════════════════
+   ACL Management Page
+   ═══════════════════════════════════════════ */
+const AclPage = () => {
+  const { t } = useLang();
+
+  /* ─── State ─── */
+  const [rules, setRules] = useState<AclRule[]>(aclRules);
+  const [users, setUsers] = useState<AclUser[]>(aclUsers);
+  const [activeTab, setActiveTab] = useState('rules');
+
+  // Rule filters
+  const [ruleSearch, setRuleSearch] = useState('');
+  const [ruleVersionFilter, setRuleVersionFilter] = useState<string>('all');
+  const [ruleDecisionFilter, setRuleDecisionFilter] = useState<string>('all');
+
+  // Rule modal
+  const [ruleModalOpen, setRuleModalOpen] = useState(false);
+  const [editingRule, setEditingRule] = useState<AclRule | null>(null);
+  const [ruleForm] = Form.useForm();
+
+  // User modal
+  const [userModalOpen, setUserModalOpen] = useState(false);
+  const [editingUser, setEditingUser] = useState<AclUser | null>(null);
+  const [userForm] = Form.useForm();
+
+  // Secret key reveal
+  const [revealedKeys, setRevealedKeys] = useState<Set<string>>(new Set());
+
+  /* ─── Filtered rules ─── */
+  const filteredRules = rules.filter((r) => {
+    const matchSearch =
+      !ruleSearch ||
+      r.principal.toLowerCase().includes(ruleSearch.toLowerCase()) ||
+      r.resource.toLowerCase().includes(ruleSearch.toLowerCase());
+    const matchVersion = ruleVersionFilter === 'all' || r.aclVersion === ruleVersionFilter;
+    const matchDecision = ruleDecisionFilter === 'all' || r.decision === ruleDecisionFilter;
+    return matchSearch && matchVersion && matchDecision;
+  });
+
+  /* ─── Rule helpers ─── */
+  const isAdmin = (principal: string) =>
+    users.find((u) => u.username === principal)?.admin ?? false;
+
+  const actionTagColor: Record<string, string> = {
+    PUB: 'blue',
+    SUB: 'green',
+    ALL: 'purple',
+  };
+
+  const actionLabel: Record<string, string> = {
+    PUB: t('acl.pub'),
+    SUB: t('acl.sub'),
+    ALL: t('acl.all'),
+  };
+
+  const openAddRuleModal = () => {
+    setEditingRule(null);
+    ruleForm.resetFields();
+    ruleForm.setFieldsValue({
+      resourcePattern: 'PREFIX',
+      actions: ['PUB'],
+      decision: 'ALLOW',
+      scope: 'cluster',
+    });
+    setRuleModalOpen(true);
+  };
+
+  const openEditRuleModal = (rule: AclRule) => {
+    setEditingRule(rule);
+    ruleForm.setFieldsValue({
+      principal: rule.principal,
+      resourceType: rule.resourceType,
+      resource: rule.resource,
+      resourcePattern: rule.resourcePattern,
+      actions: rule.actions,
+      decision: rule.decision,
+      scope: rule.scope,
+    });
+    setRuleModalOpen(true);
+  };
+
+  const handleRuleSubmit = () => {
+    ruleForm.validateFields().then((values) => {
+      if (editingRule) {
+        setRules((prev) => prev.map((r) => (r.id === editingRule.id ? { ...r, ...values } : r)));
+        message.success(t('acl.ruleUpdated'));
+      } else {
+        const newRule: AclRule = {
+          id: `acl-${Date.now()}`,
+          ...values,
+          aclVersion: '2.0',
+          createdAt: new Date().toISOString(),
+        };
+        setRules((prev) => [newRule, ...prev]);
+        message.success(t('acl.ruleAdded'));
+      }
+      setRuleModalOpen(false);
+    });
+  };
+
+  const handleDeleteRule = (id: string) => {
+    setRules((prev) => prev.filter((r) => r.id !== id));
+    message.success(t('acl.ruleDeleted'));
+  };
+
+  /* ─── User helpers ─── */
+  const toggleRevealKey = (userId: string) => {
+    setRevealedKeys((prev) => {
+      const next = new Set(prev);
+      if (next.has(userId)) {
+        next.delete(userId);
+      } else {
+        next.add(userId);
+      }
+      return next;
+    });
+  };
+
+  const openAddUserModal = () => {
+    setEditingUser(null);
+    userForm.resetFields();
+    userForm.setFieldsValue({ admin: false });
+    setUserModalOpen(true);
+  };
+
+  const openEditUserModal = (user: AclUser) => {
+    setEditingUser(user);
+    userForm.setFieldsValue({
+      username: user.username,
+      accessKey: user.accessKey,
+      secretKey: user.secretKey,
+      admin: user.admin,
+    });
+    setUserModalOpen(true);
+  };
+
+  const handleUserSubmit = () => {
+    userForm.validateFields().then((values) => {
+      if (editingUser) {
+        setUsers((prev) => prev.map((u) => (u.id === editingUser.id ? { ...u, ...values } : u)));
+        message.success(t('acl.userUpdated'));
+      } else {
+        const newUser: AclUser = {
+          id: `u-${Date.now()}`,
+          username: values.username,
+          accessKey: values.accessKey || `LTAI****${values.username.slice(-4)}`,
+          secretKey:
+            values.secretKey ||
+            `${Math.random().toString(36).slice(2, 6)}****${Math.random().toString(36).slice(2, 6)}`,
+          admin: values.admin ?? false,
+          clusters: ['rmq-cn-v5-prod-01'],
+          createdAt: new Date().toISOString(),
+        };
+        setUsers((prev) => [newUser, ...prev]);
+        message.success(t('acl.userAdded'));
+      }
+      setUserModalOpen(false);
+    });
+  };
+
+  const handleDeleteUser = (id: string) => {
+    setUsers((prev) => prev.filter((u) => u.id !== id));
+    message.success(t('acl.userDeleted'));
+  };
+
+  const handleToggleAdmin = (userId: string, checked: boolean) => {
+    setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, admin: checked } : u)));
+    message.success(checked ? t('acl.adminSet') : t('acl.adminRemoved'));
+  };
+
+  const formatDate = (iso: string) => {
+    const d = new Date(iso);
+    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
+  };
+
+  /* ═══════════════════════════════════════════
+     ACL Rules Table
+     ═══════════════════════════════════════════ */
+  const ruleColumns: ColumnsType<AclRule> = [
+    {
+      title: t('acl.principal'),
+      dataIndex: 'principal',
+      key: 'principal',
+      width: 200,
+      sorter: (a, b) => a.principal.localeCompare(b.principal),
+      render: (text: string) => (
+        <Space size={6}>
+          <User size={14} color="#8c8c8c" weight="fill" />
+          <span style={{ fontWeight: 500 }}>{text}</span>
+          {isAdmin(text) && (
+            <Badge
+              count={t('acl.adminBadge')}
+              style={{ backgroundColor: '#722ed1', fontSize: 11 }}
+            />
+          )}
+        </Space>
+      ),
+    },
+    {
+      title: t('acl.resource'),
+      key: 'resource',
+      width: 240,
+      sorter: (a, b) => a.resource.localeCompare(b.resource),
+      render: (_: unknown, record: AclRule) => (
+        <Space size={6}>
+          <span style={{ fontFamily: 'monospace', fontWeight: 500 }}>{record.resource}</span>
+          <Tag
+            color={record.resourcePattern === 'LITERAL' ? 'blue' : 'green'}
+            style={{ fontSize: 11, lineHeight: '18px' }}
+          >
+            {record.resourcePattern}
+          </Tag>
+        </Space>
+      ),
+    },
+    {
+      title: t('acl.permissions'),
+      dataIndex: 'actions',
+      key: 'actions',
+      width: 180,
+      render: (actions: AclRule['actions']) => (
+        <Space size={4} wrap>
+          {actions.map((action) => (
+            <Tag key={action} color={actionTagColor[action]} style={{ fontSize: 11 }}>
+              {actionLabel[action] ?? action}
+            </Tag>
+          ))}
+        </Space>
+      ),
+    },
+    {
+      title: t('acl.decision'),
+      dataIndex: 'decision',
+      key: 'decision',
+      width: 80,
+      sorter: (a, b) => a.decision.localeCompare(b.decision),
+      render: (decision: string) => (
+        <Tag color={decision === 'ALLOW' ? 'green' : 'red'} style={{ fontWeight: 600 }}>
+          {decision === 'ALLOW' ? t('acl.allow') : t('acl.deny')}
+        </Tag>
+      ),
+    },
+    {
+      title: t('acl.aclVersion'),
+      dataIndex: 'aclVersion',
+      key: 'aclVersion',
+      width: 100,
+      sorter: (a, b) => a.aclVersion.localeCompare(b.aclVersion),
+      render: (version: string) => (
+        <Tag color={version === '2.0' ? 'geekblue' : 'default'}>{version}</Tag>
+      ),
+    },
+    {
+      title: t('acl.scope'),
+      dataIndex: 'scope',
+      key: 'scope',
+      width: 100,
+      sorter: (a, b) => a.scope.localeCompare(b.scope),
+      render: (scope: string) => (
+        <span style={{ fontSize: 13 }}>
+          {scope === 'cluster' ? t('acl.cluster') : t('acl.namespace')}
+        </span>
+      ),
+    },
+    {
+      title: t('acl.createdAt'),
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 160,
+      sorter: (a, b) => a.createdAt.localeCompare(b.createdAt),
+      render: (iso: string) => (
+        <span style={{ fontSize: 13, color: '#8c8c8c' }}>{formatDate(iso)}</span>
+      ),
+    },
+    {
+      title: t('common.actions'),
+      key: 'ruleActions',
+      width: 160,
+      render: (_: unknown, record: AclRule) => (
+        <Flex gap={6}>
+          <Button
+            size="small"
+            icon={<EditOutlined />}
+            style={{ borderColor: '#1677ff', color: '#1677ff' }}
+            onClick={() => openEditRuleModal(record)}
+          >
+            {t('common.edit')}
+          </Button>
+          <Button
+            size="small"
+            icon={<DeleteOutlined />}
+            style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
+            onClick={() =>
+              Modal.confirm({
+                title: t('acl.confirmDeleteRule'),
+                content: t('acl.deleteWarning'),
+                okText: t('common.delete'),
+                okButtonProps: { danger: true },
+                onOk: () => handleDeleteRule(record.id),
+              })
+            }
+          >
+            {t('common.delete')}
+          </Button>
+        </Flex>
+      ),
+    },
+  ];
+
+  /* ═══════════════════════════════════════════
+     Users Table
+     ═══════════════════════════════════════════ */
+  const userColumns: ColumnsType<AclUser> = [
+    {
+      title: t('acl.username'),
+      dataIndex: 'username',
+      key: 'username',
+      width: 200,
+      sorter: (a, b) => a.username.localeCompare(b.username),
+      render: (text: string, record: AclUser) => (
+        <Space size={6}>
+          <User size={14} color="#8c8c8c" weight="fill" />
+          <span style={{ fontWeight: 500 }}>{text}</span>
+          {record.admin && (
+            <Badge
+              count={t('acl.adminBadge')}
+              style={{ backgroundColor: '#722ed1', fontSize: 11 }}
+            />
+          )}
+        </Space>
+      ),
+    },
+    {
+      title: 'Access Key',
+      dataIndex: 'accessKey',
+      key: 'accessKey',
+      width: 220,
+      sorter: (a, b) => a.accessKey.localeCompare(b.accessKey),
+      render: (text: string, record: AclUser) => {
+        const revealed = revealedKeys.has(record.id);
+        return (
+          <Space size={8}>
+            <Typography.Text copyable={{ text }} style={{ fontFamily: 'monospace', fontSize: 13 }}>
+              {revealed ? text : text.replace(/(?<=\*{4}).+/, '••••')}
+            </Typography.Text>
+            <Button
+              type="text"
+              size="small"
+              icon={revealed ? <EyeSlash size={14} /> : <Eye size={14} />}
+              onClick={() => toggleRevealKey(record.id)}
+            />
+          </Space>
+        );
+      },
+    },
+    {
+      title: t('acl.admin'),
+      dataIndex: 'admin',
+      key: 'admin',
+      width: 100,
+      sorter: (a, b) => Number(a.admin) - Number(b.admin),
+      render: (val: boolean, record: AclUser) => (
+        <Switch
+          checked={val}
+          size="small"
+          onChange={(checked) => handleToggleAdmin(record.id, checked)}
+        />
+      ),
+    },
+    {
+      title: t('acl.associatedClusters'),
+      dataIndex: 'clusters',
+      key: 'clusters',
+      width: 280,
+      sorter: (a, b) => a.clusters.length - b.clusters.length,
+      render: (clusters: string[]) => (
+        <Space size={4} wrap>
+          {clusters.map((c) => (
+            <Tag key={c} color="processing" style={{ fontSize: 11 }}>
+              {c}
+            </Tag>
+          ))}
+        </Space>
+      ),
+    },
+    {
+      title: t('acl.createdAt'),
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 160,
+      sorter: (a, b) => a.createdAt.localeCompare(b.createdAt),
+      render: (iso: string) => (
+        <span style={{ fontSize: 13, color: '#8c8c8c' }}>{formatDate(iso)}</span>
+      ),
+    },
+    {
+      title: t('common.actions'),
+      key: 'userActions',
+      width: 160,
+      render: (_: unknown, record: AclUser) => (
+        <Flex gap={6}>
+          <Button
+            size="small"
+            icon={<EditOutlined />}
+            style={{ borderColor: '#1677ff', color: '#1677ff' }}
+            onClick={() => openEditUserModal(record)}
+          >
+            {t('common.edit')}
+          </Button>
+          <Button
+            size="small"
+            icon={<DeleteOutlined />}
+            style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
+            onClick={() =>
+              Modal.confirm({
+                title: t('acl.confirmDeleteUser'),
+                content: t('acl.deleteUserWarning'),
+                okText: t('common.delete'),
+                okButtonProps: { danger: true },
+                onOk: () => handleDeleteUser(record.id),
+              })
+            }
+          >
+            {t('common.delete')}
+          </Button>
+        </Flex>
+      ),
+    },
+  ];
+
+  /* ═══════════════════════════════════════════
+     Render
+     ═══════════════════════════════════════════ */
+  return (
+    <div style={{ padding: 24 }}>
+      <PageHeader
+        title={t('acl.title')}
+        subtitle={t('acl.subtitle', { rules: rules.length, users: users.length })}
+        extra={
+          <Button
+            type="primary"
+            icon={<Plus size={14} weight="bold" />}
+            onClick={activeTab === 'rules' ? openAddRuleModal : openAddUserModal}
+          >
+            {activeTab === 'rules' ? t('acl.addRule') : t('acl.addUser')}
+          </Button>
+        }
+      />
+
+      <Card bordered={false} bodyStyle={{ padding: 0 }}>
+        <Tabs
+          activeKey={activeTab}
+          onChange={setActiveTab}
+          style={{ padding: '0 16px' }}
+          items={[
+            {
+              key: 'rules',
+              label: (
+                <Space size={6}>
+                  <ShieldCheck size={15} />
+                  <span>{t('acl.ruleTab')}</span>
+                </Space>
+              ),
+              children: (
+                <div>
+                  {/* Filter bar */}
+                  <div
+                    style={{
+                      display: 'flex',
+                      gap: 12,
+                      padding: '16px 0',
+                      flexWrap: 'wrap',
+                    }}
+                  >
+                    <Input.Search
+                      placeholder={t('acl.searchPrincipal')}
+                      prefix={<MagnifyingGlass size={14} color="#9CA3AF" />}
+                      value={ruleSearch}
+                      onChange={(e) => setRuleSearch(e.target.value)}
+                      onSearch={setRuleSearch}
+                      allowClear
+                      style={{ width: 260 }}
+                    />
+                    <Select
+                      value={ruleVersionFilter}
+                      onChange={setRuleVersionFilter}
+                      style={{ width: 140 }}
+                      options={[
+                        { value: 'all', label: t('acl.allVersions') },
+                        { value: '1.0', label: 'ACL 1.0' },
+                        { value: '2.0', label: 'ACL 2.0' },
+                      ]}
+                    />
+                    <Select
+                      value={ruleDecisionFilter}
+                      onChange={setRuleDecisionFilter}
+                      style={{ width: 140 }}
+                      options={[
+                        { value: 'all', label: t('acl.allDecisions') },
+                        { value: 'ALLOW', label: t('acl.allow') },
+                        { value: 'DENY', label: t('acl.deny') },
+                      ]}
+                    />
+                  </div>
+
+                  {/* Rules table */}
+                  <Table
+                    columns={ruleColumns}
+                    dataSource={filteredRules}
+                    rowKey="id"
+                    pagination={{
+                      pageSize: 20,
+                      showSizeChanger: true,
+                      showTotal: (total) => t('acl.totalRules', { n: total }),
+                    }}
+                    size="small"
+                  />
+                </div>
+              ),
+            },
+            {
+              key: 'users',
+              label: (
+                <Space size={6}>
+                  <User size={15} />
+                  <span>{t('acl.userTab')}</span>
+                </Space>
+              ),
+              children: (
+                <div>
+                  <div style={{ padding: '16px 0' }}>
+                    <Space>
+                      <Button
+                        type="primary"
+                        icon={<Plus size={14} weight="bold" />}
+                        onClick={openAddUserModal}
+                      >
+                        {t('acl.addUser')}
+                      </Button>
+                    </Space>
+                  </div>
+
+                  <Table
+                    columns={userColumns}
+                    dataSource={users}
+                    rowKey="id"
+                    pagination={{
+                      pageSize: 20,
+                      showSizeChanger: true,
+                      showTotal: (total) => t('acl.totalUsers', { n: total }),
+                    }}
+                    size="small"
+                  />
+                </div>
+              ),
+            },
+          ]}
+        />
+      </Card>
+
+      {/* ─── Add/Edit Rule Modal ─── */}
+      <Modal
+        title={editingRule ? t('acl.editRule') : t('acl.addRule')}
+        open={ruleModalOpen}
+        onCancel={() => setRuleModalOpen(false)}
+        onOk={handleRuleSubmit}
+        okText={editingRule ? t('acl.save') : t('acl.add')}
+        cancelText={t('common.cancel')}
+        width={560}
+        destroyOnClose
+      >
+        <Form form={ruleForm} layout="vertical" style={{ marginTop: 16 }}>
+          <Form.Item
+            name="principal"
+            label={t('acl.principal')}
+            rules={[{ required: true, message: t('acl.required', { field: t('acl.principal') }) }]}
+          >
+            <Select
+              placeholder={t('acl.selectPrincipal')}
+              showSearch
+              optionFilterProp="label"
+              options={users.map((u) => ({
+                value: u.username,
+                label: u.username,
+              }))}
+            />
+          </Form.Item>
+
+          <Form.Item
+            name="resourceType"
+            label={t('acl.resourceType')}
+            rules={[
+              { required: true, message: t('acl.required', { field: t('acl.resourceType') }) },
+            ]}
+          >
+            <Select
+              placeholder={t('acl.selectResourceType')}
+              options={[
+                { value: 'Topic', label: 'Topic' },
+                { value: 'Group', label: 'Group' },
+                { value: 'Cluster', label: 'Cluster' },
+              ]}
+            />
+          </Form.Item>
+
+          <Form.Item
+            name="resource"
+            label={t('acl.resourceName')}
+            rules={[
+              { required: true, message: t('acl.inputRequired', { field: t('acl.resourceName') }) },
+            ]}
+          >
+            <Input placeholder={t('acl.resourceNamePlaceholder')} />
+          </Form.Item>
+
+          <Form.Item name="resourcePattern" label={t('acl.matchPattern')}>
+            <Radio.Group>
+              <Radio.Button value="LITERAL">{t('acl.literal')}</Radio.Button>
+              <Radio.Button value="PREFIX">{t('acl.prefix')}</Radio.Button>
+            </Radio.Group>
+          </Form.Item>
+
+          <Form.Item
+            name="actions"
+            label={t('acl.permissions')}
+            rules={[
+              { required: true, message: t('acl.required', { field: t('acl.permissions') }) },
+            ]}
+          >
+            <Checkbox.Group>
+              <Space>
+                <Checkbox value="PUB">{t('acl.pub')}</Checkbox>
+                <Checkbox value="SUB">{t('acl.sub')}</Checkbox>
+                <Checkbox value="ALL">{t('acl.all')}</Checkbox>
+              </Space>
+            </Checkbox.Group>
+          </Form.Item>
+
+          <Form.Item name="decision" label={t('acl.decision')}>
+            <Radio.Group>
+              <Radio.Button value="ALLOW">
+                <span style={{ color: '#52c41a' }}>{t('acl.allowDesc')}</span>
+              </Radio.Button>
+              <Radio.Button value="DENY">
+                <span style={{ color: '#ff4d4f' }}>{t('acl.denyDesc')}</span>
+              </Radio.Button>
+            </Radio.Group>
+          </Form.Item>
+
+          <Form.Item name="scope" label={t('acl.effectScope')}>
+            <Select
+              placeholder={t('acl.selectEffectScope')}
+              options={[
+                { value: 'cluster', label: t('acl.clusterScope') },
+                { value: 'namespace', label: t('acl.namespaceScope') },
+              ]}
+            />
+          </Form.Item>
+        </Form>
+      </Modal>
+
+      {/* ─── Add/Edit User Modal ─── */}
+      <Modal
+        title={editingUser ? t('acl.editUser') : t('acl.addUser')}
+        open={userModalOpen}
+        onCancel={() => setUserModalOpen(false)}
+        onOk={handleUserSubmit}
+        okText={editingUser ? t('acl.save') : t('acl.add')}
+        cancelText={t('common.cancel')}
+        width={520}
+        destroyOnClose
+      >
+        <Form form={userForm} layout="vertical" style={{ marginTop: 16 }}>
+          <Form.Item
+            name="username"
+            label={t('acl.username')}
+            rules={[
+              { required: true, message: t('acl.inputRequired', { field: t('acl.username') }) },
+            ]}
+          >
+            <Input
+              placeholder={t('acl.usernamePlaceholder')}
+              disabled={!!editingUser}
+              prefix={<User size={14} color="#9CA3AF" />}
+            />
+          </Form.Item>
+
+          <Form.Item name="accessKey" label="Access Key">
+            <Input placeholder={t('acl.autoOrManual')} style={{ fontFamily: 'monospace' }} />
+          </Form.Item>
+
+          <Form.Item name="secretKey" label="Secret Key">
+            <Input.Password
+              placeholder={t('acl.autoOrManual')}
+              style={{ fontFamily: 'monospace' }}
+            />
+          </Form.Item>
+
+          <Form.Item name="admin" label={t('acl.admin')} valuePropName="checked">
+            <Switch checkedChildren={t('common.yes')} unCheckedChildren={t('common.no')} />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default AclPage;
diff --git a/web/src/pages/instance/consumer.tsx b/web/src/pages/instance/consumer.tsx
new file mode 100644
index 0000000..9e0ec7c
--- /dev/null
+++ b/web/src/pages/instance/consumer.tsx
@@ -0,0 +1,1062 @@
+/*
+ * 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 { useState, useMemo } from 'react';
+import {
+  Table,
+  Card,
+  Button,
+  Tag,
+  Space,
+  Input,
+  Select,
+  Tabs,
+  Modal,
+  Form,
+  Descriptions,
+  Statistic,
+  Radio,
+  InputNumber,
+  Typography,
+  Row,
+  Col,
+  Flex,
+  DatePicker,
+  message,
+} from 'antd';
+import {
+  Plus,
+  MagnifyingGlass,
+  Eye,
+  ArrowsCounterClockwise,
+  Trash,
+  Clock,
+  Cube,
+  Users,
+  ListBullets,
+  Info,
+  ArrowsClockwise,
+} from '@phosphor-icons/react';
+import { ImportOutlined, ExportOutlined, DeleteOutlined } from '@ant-design/icons';
+import type { ColumnsType } from 'antd/es/table';
+import dayjs from 'dayjs';
+import type { Dayjs } from 'dayjs';
+
+import PageHeader from '../../components/PageHeader';
+import { useLang } from '../../i18n/LangContext';
+import { TOPIC_TYPE_MAP, PROTOCOL_MAP } from '../../constants/theme';
+import { mockConsumerGroups, mockQueueProgress, mockSubscriptions } from '../../mock/consumers';
+import type {
+  ConsumerGroup,
+  ConsumerInstance,
+  QueueProgress,
+  SubscriptionEntry,
+} from '../../mock/consumers';
+
+const { Text } = Typography;
+
+/* ─── Helpers ─── */
+
+const lagColor = (lag: number): string => {
+  if (lag >= 10_000) return '#ff4d4f';
+  if (lag >= 1_000) return '#faad14';
+  return '#52c41a';
+};
+
+/**
+ * Format delay seconds into human-readable Chinese time.
+ * Shows at most 3 units: days → hours → minutes → seconds.
+ * e.g. 82500 → "22小时55分钟", 3725 → "1小时2分钟5秒"
+ */
+const formatDelay = (totalSeconds: number): string => {
+  if (totalSeconds <= 0) return '0秒';
+
+  const days = Math.floor(totalSeconds / 86400);
+  let remaining = totalSeconds % 86400;
+  const hours = Math.floor(remaining / 3600);
+  remaining %= 3600;
+  const minutes = Math.floor(remaining / 60);
+  const seconds = remaining % 60;
+
+  const parts: string[] = [];
+  if (days > 0) parts.push(`${days}天`);
+  if (hours > 0) parts.push(`${hours}小时`);
+  if (minutes > 0) parts.push(`${minutes}分钟`);
+  if (seconds > 0 && parts.length < 3) parts.push(`${seconds}秒`);
+
+  return parts.length > 0 ? parts.join('') : '0秒';
+};
+
+const formatDateTime = (dateStr: string): string => {
+  const d = new Date(dateStr);
+  const pad = (n: number) => String(n).padStart(2, '0');
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+};
+
+/* ═══════════════════════════════════════════
+   ConsumerPage
+   ═══════════════════════════════════════════ */
+const ConsumerPage = () => {
+  const { t } = useLang();
+  const [groups, setGroups] = useState<ConsumerGroup[]>(mockConsumerGroups);
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [search, setSearch] = useState('');
+  const [modeFilter, setModeFilter] = useState<string>('ALL');
+  const [sortKey, setSortKey] = useState<string>('name_asc');
+  const [modalOpen, setModalOpen] = useState(false);
+  const [selectedGroup, setSelectedGroup] = useState<ConsumerGroup | null>(null);
+  const [createModalOpen, setCreateModalOpen] = useState(false);
+  const [form] = Form.useForm();
+  const [dataTypeValue, setDataTypeValue] = useState<string | undefined>(undefined);
+  const [resetModalOpen, setResetModalOpen] = useState(false);
+  const [resetGroup, setResetGroup] = useState<ConsumerGroup | null>(null);
+  const [resetTime, setResetTime] = useState<Dayjs>(dayjs().subtract(3, 'hour'));
+
+  /* ─── Filtered & sorted data ─── */
+  const filtered = useMemo(() => {
+    let data = groups.filter(
+      (g) =>
+        g.name.includes(search) ||
+        g.namespace.includes(search) ||
+        g.subscribedTopics.some((t) => t.includes(search)),
+    );
+
+    if (modeFilter !== 'ALL') {
+      data = data.filter((g) => g.subscriptionMode === modeFilter);
+    }
+
+    if (sortKey === 'lag_desc') {
+      data = [...data].sort((a, b) => b.totalLag - a.totalLag);
+    } else if (sortKey === 'name_asc') {
+      data = [...data].sort((a, b) => a.name.localeCompare(b.name));
+    }
+
+    return data;
+  }, [groups, search, modeFilter, sortKey]);
+
+  /* ─── Open detail modal ─── */
+  const openModal = (group: ConsumerGroup) => {
+    setSelectedGroup(group);
+    setModalOpen(true);
+  };
+
+  /* ═══════════════════════════════════════════
+     Main Table Columns
+     ═══════════════════════════════════════════ */
+  const columns: ColumnsType<ConsumerGroup> = [
+    {
+      title: 'Group 名称',
+      dataIndex: 'name',
+      key: 'name',
+      width: 220,
+      sorter: (a, b) => a.name.localeCompare(b.name),
+      render: (name: string) => (
+        <Text strong style={{ fontSize: 14 }}>
+          {name}
+        </Text>
+      ),
+    },
+    {
+      title: '订阅组类型',
+      dataIndex: 'subscriptionDataType',
+      key: 'subscriptionDataType',
+      width: 110,
+      sorter: (a, b) => a.subscriptionDataType.localeCompare(b.subscriptionDataType),
+      render: (type: string) => {
+        const config = TOPIC_TYPE_MAP[type] || { label: type, color: 'default' };
+        return <Tag color={config.color}>{config.label}</Tag>;
+      },
+    },
+    {
+      title: '订阅模式',
+      dataIndex: 'subscriptionMode',
+      key: 'subscriptionMode',
+      width: 90,
+      sorter: (a, b) => a.subscriptionMode.localeCompare(b.subscriptionMode),
+      render: (mode: string) => <Tag color={mode === 'Push' ? 'blue' : 'green'}>{mode}</Tag>,
+    },
+    {
+      title: '在线客户端',
+      dataIndex: 'onlineInstances',
+      key: 'onlineInstances',
+      width: 130,
+      align: 'center',
+      sorter: (a, b) => a.onlineInstances - b.onlineInstances,
+    },
+    {
+      title: '总堆积量',
+      dataIndex: 'totalLag',
+      key: 'totalLag',
+      width: 120,
+      sorter: (a, b) => a.totalLag - b.totalLag,
+      render: (lag: number) => lag.toLocaleString(),
+    },
+    {
+      title: '消费延迟',
+      dataIndex: 'delaySeconds',
+      key: 'delaySeconds',
+      width: 160,
+      sorter: (a, b) => a.delaySeconds - b.delaySeconds,
+      render: (seconds: number) => formatDelay(seconds),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 170,
+      sorter: (a, b) => a.createdAt.localeCompare(b.createdAt),
+      render: (d: string) => (
+        <Text type="secondary" style={{ fontSize: 13 }}>
+          {d}
+        </Text>
+      ),
+    },
+    {
+      title: '修改时间',
+      dataIndex: 'updatedAt',
+      key: 'updatedAt',
+      width: 170,
+      sorter: (a, b) => a.updatedAt.localeCompare(b.updatedAt),
+      render: (d: string) => (
+        <Text type="secondary" style={{ fontSize: 13 }}>
+          {d}
+        </Text>
+      ),
+    },
+    {
+      title: '操作',
+      key: 'actions',
+      width: 240,
+      render: (_: unknown, record: ConsumerGroup) => (
+        <Flex gap={6}>
+          <Button
+            size="small"
+            icon={<Eye size={14} />}
+            style={{ borderColor: '#1677ff', color: '#1677ff' }}
+            onClick={(e) => {
+              e.stopPropagation();
+              openModal(record);
+            }}
+          >
+            详情
+          </Button>
+          <Button
+            size="small"
+            icon={<ArrowsCounterClockwise size={14} />}
+            style={{ borderColor: '#fa8c16', color: '#fa8c16' }}
+            onClick={(e) => {
+              e.stopPropagation();
+              setResetGroup(record);
+              setResetTime(dayjs().subtract(3, 'hour'));
+              setResetModalOpen(true);
+            }}
+          >
+            重置位点
+          </Button>
+          <Button
+            size="small"
+            icon={<Trash size={14} />}
+            style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
+            onClick={(e) => {
+              e.stopPropagation();
+              Modal.confirm({
+                title: `确认删除消费组 "${record.name}"?`,
+                content: '删除后该消费组的所有配置和消费进度将被清除,此操作不可恢复。',
+                okText: '删除',
+                okButtonProps: { danger: true },
+                cancelText: '取消',
+                onOk: () => message.success(`消费组 ${record.name} 已删除`),
+              });
+            }}
+          >
+            删除
+          </Button>
+        </Flex>
+      ),
+    },
+  ];
+
+  /* ═══════════════════════════════════════════
+     Expandable Sub-table: Subscription Details
+     ═══════════════════════════════════════════ */
+  const subscriptionSubColumns: ColumnsType<SubscriptionEntry> = [
+    {
+      title: 'Topic 主题',
+      dataIndex: 'topic',
+      key: 'topic',
+      width: 200,
+      render: (name: string) => (
+        <Text strong style={{ fontSize: 13 }}>
+          {name}
+        </Text>
+      ),
+    },
+    {
+      title: '订阅一致性',
+      dataIndex: 'consistency',
+      key: 'consistency',
+      width: 110,
+      render: (v: string) => <Tag color={v === '一致' ? 'green' : 'orange'}>{v}</Tag>,
+    },
+    {
+      title: '订阅模式',
+      dataIndex: 'filterMode',
+      key: 'filterMode',
+      width: 120,
+      render: (mode: string) => {
+        const colorMap: Record<string, string> = {
+          全量: 'default',
+          'Tag 过滤': 'blue',
+          'SQL92 过滤': 'purple',
... 4735 lines suppressed ...


(rocketmq-dashboard) 09/09: docs: add deployment config and bilingual README

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit ef1596d0f15d45583dca9969155b095cc2104633
Author: lizhimins <70...@qq.com>
AuthorDate: Wed Jul 8 10:12:00 2026 +0800

    docs: add deployment config and bilingual README
    
    - Add Docker Compose deployment setup
    - Add Nginx reverse proxy configuration
    - Add deployment scripts and env templates
    - Add API specification document
    - Write bilingual README (English + Chinese)
    - Add project overview, architecture, and quick start guide
---
 README.md                 |   56 ++
 README_zh.md              |   56 ++
 deploy/.env.example       |    8 +
 deploy/README.md          |   28 +
 deploy/deploy.sh          |  185 +++++
 deploy/docker-compose.yml |   24 +
 deploy/nginx.conf         |   20 +
 docs/api-spec.md          | 1696 +++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 2073 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b4260eb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+# RocketMQ Studio
+
+**English** | [中文](README_zh.md)
+
+> Cross-cluster · Cross-architecture · Cross-cloud unified RocketMQ management platform
+
+RocketMQ Studio is a unified management platform for RocketMQ, supporting multi-cluster, multi-architecture, and multi-cloud environments. It provides instance management, cluster operations, Topic / Consumer Group CRUD, ACL permission control, message query and tracing, dead letter queue handling, monitoring alerts, audit logs, and an AI assistant.
+
+## Quick Start
+
+```bash
+cd deploy && docker compose up -d --build
+```
+
+Visit **http://127.0.0.1:6789** after startup.
+
+**Studio ports:** Frontend 6789 (Nginx), Backend 8888 (Spring Boot)
+
+**RocketMQ ports:** NameServer 9876, Broker 10911, Proxy Remoting 8080, Proxy gRPC 8081
+
+## Features
+
+| Module | Capabilities |
+|--------|--------------|
+| **Dashboard** | Global statistics for clusters, brokers, topics, and consumer groups with TPS trends |
+| **Instances** | Multi-instance access (Proxy / Direct mode), instance CRUD |
+| **Clusters** | Cluster details, Broker / NameServer / Proxy node operations, hot config updates |
+| **K8s Certs** | TLS / mTLS / ServiceAccount certificate management and renewal |
+| **Topics** | Topic CRUD, route viewer, consumer list, multi-type support (Normal / FIFO / Delay / Transaction / Lite) |
+| **Consumer Groups** | Consumer group CRUD, consumption progress, subscription details, offset reset, config import/export |
+| **ACL** | ACL rules and user management, v1 / v2 dual version support |
+| **Messages** | Query by Topic / MsgId / Key / time range, message trace visualization |
+| **Dead Letter Queue** | DLQ message viewing and batch resend |
+| **Clients** | Producer / Consumer online connection list, protocol and language version stats |
+| **Alert Rules** | Multi-dimensional alert rules (disk / lag / TPS / node offline), DingTalk / email / SMS notifications |
+| **System Alerts** | System-level alert viewing, acknowledgment, and cleanup |
+| **Audit Logs** | Operation audit log query, filter by type / time / result, history cleanup |
+| **AI Assistant** | SSE streaming chat, supports query / diagnose / manage / general modes, MCP tool integration |
+| **Settings** | General preferences, LLM config, datasource management |
+
+## Tech Stack
+
+- **Frontend** — React 18 + TypeScript + Vite + Ant Design + Tailwind CSS
+- **Backend** — Java 21 + Spring Boot 3.5 + Hexagonal Architecture (ArchUnit enforced)
+- **Deployment** — Docker multi-stage builds, Nginx reverse proxy, Docker Compose or `deploy.sh` for remote deployment
+
+## Development Guidelines
+
+- **Code Style** — ESLint + Prettier for frontend, Husky pre-commit hook for auto-check
+- **Commit Format** — Conventional Commits (`feat:` / `fix:` / `refactor:` / `chore:` / `docs:` / `perf:`)
+- **Architecture Tests** — `mvn test` runs ArchUnit hexagonal architecture constraint checks
+- **i18n** — New frontend text must include both Chinese and English translations (`web/src/i18n/`)
+
+## License
+
+[Apache License 2.0](LICENSE)
diff --git a/README_zh.md b/README_zh.md
new file mode 100644
index 0000000..a2a9319
--- /dev/null
+++ b/README_zh.md
@@ -0,0 +1,56 @@
+# RocketMQ Studio
+
+[English](README.md) | **中文**
+
+> 跨集群 · 跨架构 · 跨云的 RocketMQ 统一管控平台
+
+RocketMQ Studio 是一个面向多集群、多架构、多云环境的 RocketMQ 管控平台,提供实例管理、集群运维、Topic / 消费组 CRUD、ACL 权限管控、消息查询与轨迹追踪、死信队列处理、监控告警、审计日志以及 AI 智能助手等一站式能力。
+
+## 一键构建 & 运行
+
+```bash
+cd deploy && docker compose up -d --build
+```
+
+启动后访问 **http://127.0.0.1:6789** 即可使用。
+
+**Studio 服务端口:** 前端 6789(Nginx)、后端 8888(Spring Boot)
+
+**RocketMQ 服务端端口:** NameServer 9876、Broker 10911、Proxy Remoting 8080、Proxy gRPC 8081
+
+## 功能概览
+
+| 模块 | 能力 |
+|------|------|
+| **监控面板** | 集群/ Broker / Topic / 消费组全局统计,TPS 趋势图 |
+| **实例管理** | 多实例接入(Proxy / Direct 模式),实例 CRUD |
+| **集群管理** | 集群详情、Broker / NameServer / Proxy 节点运维、集群配置热更新 |
+| **K8s 证书** | TLS / mTLS / ServiceAccount 证书管理与续期 |
+| **Topic 管理** | Topic CRUD、路由查看、消费者列表、多类型支持(Normal / FIFO / Delay / Transaction / Lite) |
+| **消费组管理** | 消费组 CRUD、消费进度、订阅详情、位点重置、配置导入导出 |
+| **ACL 权限** | ACL 规则与用户管理,支持 v1 / v2 双版本 |
+| **消息查询** | 按 Topic / MsgId / Key / 时间范围查询,消息轨迹可视化 |
+| **死信队列** | DLQ 消息查看与批量重发 |
+| **客户端连接** | Producer / Consumer 在线连接列表,协议与语言版本统计 |
+| **告警规则** | 多维度告警规则配置(磁盘 / 堆积 / TPS / 节点离线),支持钉钉 / 邮件 / 短信通知 |
+| **系统告警** | 系统级告警查看、确认与清理 |
+| **审计日志** | 操作审计日志查询、按类型 / 时间 / 结果过滤、历史清理 |
+| **AI 助手** | SSE 流式对话,支持查询 / 诊断 / 管控 / 通用多模式,MCP 工具集成 |
+| **系统设置** | 通用偏好设置、LLM 配置、数据源管理 |
+
+## 技术栈
+
+- **前端** — React 18 + TypeScript + Vite + Ant Design + Tailwind CSS
+- **后端** — Java 21 + Spring Boot 3.5 + 六边形架构(ArchUnit 约束)
+- **部署** — Docker 多阶段构建,Nginx 反向代理,支持 Docker Compose 本地运行或 `deploy.sh` 远程部署
+
+## 开发规范
+
+- **代码风格** — 前端通过 ESLint + Prettier 统一格式,Husky pre-commit hook 自动检查
+- **Commit 格式** — 遵循 Conventional Commits(`feat:` / `fix:` / `refactor:` / `chore:` / `docs:` / `perf:`)
+- **架构测试** — 后端 `mvn test` 自动运行 ArchUnit 六边形架构约束检查
+- **国际化** — 新增前端文案需同时提供中英文翻译(`web/src/i18n/`)
+
+## License
+
+[Apache License 2.0](LICENSE)
diff --git a/deploy/.env.example b/deploy/.env.example
new file mode 100644
index 0000000..16b8e6e
--- /dev/null
+++ b/deploy/.env.example
@@ -0,0 +1,8 @@
+# RocketMQ Studio Docker Environment
+# Remote deployment target
+REMOTE_HOST=101.37.235.82
+REMOTE_USER=root
+REMOTE_PATH=/opt/rocketmq-studio
+
+# Frontend public port
+PUBLIC_PORT=6789
diff --git a/deploy/README.md b/deploy/README.md
new file mode 100644
index 0000000..c02c26a
--- /dev/null
+++ b/deploy/README.md
@@ -0,0 +1,28 @@
+# 部署
+
+## 一键部署
+
+```bash
+./deploy/deploy.sh           # 部署 server + web
+./deploy/deploy.sh server    # 仅部署后端
+./deploy/deploy.sh web       # 仅部署前端
+```
+
+脚本自动完成:本地构建 → 导出镜像 → SCP 传输 → 远程加载 → 重启容器 → 验证
+
+## 配置
+
+编辑 `deploy/.env` 修改部署目标:
+
+```env
+REMOTE_HOST=101.37.235.82
+REMOTE_USER=root
+REMOTE_PATH=/opt/rocketmq-studio
+PUBLIC_PORT=8080
+```
+
+## 前置条件
+
+- 本地安装 Docker
+- 远程机器可通过 SSH 免密登录
+- 远程机器已安装 Podman(Alibaba Cloud Linux 3 默认提供)
diff --git a/deploy/deploy.sh b/deploy/deploy.sh
new file mode 100755
index 0000000..a67c810
--- /dev/null
+++ b/deploy/deploy.sh
@@ -0,0 +1,185 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# ─── RocketMQ Studio 一键部署脚本 ───
+# 用法:
+#   ./deploy/deploy.sh           # 部署 server + web
+#   ./deploy/deploy.sh server    # 仅部署 server
+#   ./deploy/deploy.sh web       # 仅部署 web
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# 加载配置
+if [[ -f "$SCRIPT_DIR/.env" ]]; then
+  set -a
+  # shellcheck disable=SC1091
+  source "$SCRIPT_DIR/.env"
+  set +a
+fi
+
+[[ -n "${REMOTE_HOST:-}" ]] || err "REMOTE_HOST 未配置,请在 deploy/.env 中设置"
+REMOTE="${REMOTE_USER:-root}@$REMOTE_HOST"
+REMOTE_PATH="${REMOTE_PATH:-/opt/rocketmq-studio}"
+NETWORK="${PODMAN_NETWORK:-rocketmq-studio}"
+TMP_DIR="/tmp/rocketmq-studio-deploy"
+
+TARGET="${1:-all}"  # all | server | web
+
+# ─── 颜色输出 ───
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+
+log()  { echo -e "${GREEN}[✓]${NC} $*"; }
+info() { echo -e "${CYAN}[→]${NC} $*"; }
+warn() { echo -e "${YELLOW}[!]${NC} $*"; }
+err()  { echo -e "${RED}[✗]${NC} $*"; exit 1; }
+
+# ─── 前置检查 ───
+check_prereqs() {
+  info "检查前置条件..."
+  command -v docker >/dev/null 2>&1 || err "docker 未安装"
+  command -v ssh    >/dev/null 2>&1 || err "ssh 未安装"
+  command -v scp    >/dev/null 2>&1 || err "scp 未安装"
+  ssh -o ConnectTimeout=5 -o BatchMode=yes "$REMOTE" "echo ok" >/dev/null 2>&1 \
+    || err "无法 SSH 连接到 $REMOTE(请确认免密登录已配置)"
+  log "前置条件通过"
+}
+
+# ─── 构建镜像 ───
+build_server() {
+  info "构建 rocketmq-server 镜像..."
+  docker build -t rocketmq-server:latest "$PROJECT_DIR/server"
+  log "rocketmq-server 镜像构建完成"
+}
+
+build_web() {
+  info "构建 rocketmq-web 镜像..."
+  docker build -t rocketmq-web:latest "$PROJECT_DIR/web"
+  log "rocketmq-web 镜像构建完成"
+}
+
+# ─── 导出 & 传输 ───
+transfer_images() {
+  mkdir -p "$TMP_DIR"
+  local images=()
+
+  if [[ "$TARGET" == "all" || "$TARGET" == "server" ]]; then
+    images+=("rocketmq-server:latest")
+  fi
+  if [[ "$TARGET" == "all" || "$TARGET" == "web" ]]; then
+    images+=("rocketmq-web:latest")
+  fi
+
+  local tarball="$TMP_DIR/rocketmq-studio-images.tar.gz"
+  info "导出镜像: ${images[*]}"
+  docker save "${images[@]}" | gzip > "$tarball"
+  log "镜像导出完成 ($(du -h "$tarball" | cut -f1))"
+
+  info "传输到 $REMOTE:$REMOTE_PATH ..."
+  ssh "$REMOTE" "mkdir -p $REMOTE_PATH"
+  scp "$tarball" "$REMOTE:$REMOTE_PATH/"
+  log "传输完成"
+
+  # 同步部署配置文件
+  info "同步部署配置..."
+  scp "$SCRIPT_DIR/docker-compose.yml" "$REMOTE:$REMOTE_PATH/"
+  scp "$SCRIPT_DIR/nginx.conf"         "$REMOTE:$REMOTE_PATH/"
+  log "配置同步完成"
+}
+
+# ─── 远程加载 & 启动 ───
+deploy_remote() {
+  local tarball="$REMOTE_PATH/rocketmq-studio-images.tar.gz"
+
+  info "远程加载镜像..."
+  ssh "$REMOTE" "gunzip -c $tarball | podman load"
+  log "镜像加载完成"
+
+  # 确保网络存在
+  ssh "$REMOTE" "podman network exists $NETWORK 2>/dev/null || podman network create $NETWORK"
+
+  if [[ "$TARGET" == "all" || "$TARGET" == "server" ]]; then
+    info "重启 rocketmq-server..."
+    ssh "$REMOTE" "
+      podman rm -f rocketmq-server 2>/dev/null || true
+      podman run -d \
+        --name rocketmq-server \
+        --network $NETWORK \
+        --restart unless-stopped \
+        -p 8888:8888 \
+        rocketmq-server:latest
+    "
+    log "rocketmq-server 已启动"
+  fi
+
+  if [[ "$TARGET" == "all" || "$TARGET" == "web" ]]; then
+    info "重启 rocketmq-web..."
+    ssh "$REMOTE" "
+      podman rm -f rocketmq-web 2>/dev/null || true
+      podman run -d \
+        --name rocketmq-web \
+        --network $NETWORK \
+        --restart unless-stopped \
+        -p ${PUBLIC_PORT:-6789}:80 \
+        rocketmq-web:latest
+    "
+    log "rocketmq-web 已启动"
+  fi
+}
+
+# ─── 验证 ───
+verify() {
+  info "验证部署..."
+  sleep 3
+
+  local status
+  status=$(ssh "$REMOTE" "podman ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'" 2>&1)
+  echo "$status"
+
+  echo ""
+  local http_code
+  http_code=$(ssh "$REMOTE" "curl -sf -o /dev/null -w '%{http_code}' --max-time 5 http://localhost:${PUBLIC_PORT:-6789}" 2>/dev/null || echo "000")
+  if [[ "$http_code" == "200" ]]; then
+    log "前端响应正常 (HTTP $http_code)"
+  else
+    warn "前端返回 HTTP $http_code"
+  fi
+
+  echo ""
+  log "部署完成 → http://${REMOTE_HOST}:${PUBLIC_PORT:-6789}"
+}
+
+# ─── 清理临时文件 ───
+cleanup() {
+  rm -rf "$TMP_DIR"
+}
+
+# ─── 主流程 ───
+main() {
+  echo "═══════════════════════════════════════════"
+  echo "  RocketMQ Studio 部署"
+  echo "  目标: $TARGET | 远程: $REMOTE:$REMOTE_PATH"
+  echo "═══════════════════════════════════════════"
+  echo ""
+
+  check_prereqs
+
+  case "$TARGET" in
+    server) build_server ;;
+    web)    build_web ;;
+    all)    build_server && build_web ;;
+    *)      err "用法: $0 [all|server|web]" ;;
+  esac
+
+  transfer_images
+  deploy_remote
+  verify
+  cleanup
+}
+
+trap cleanup EXIT
+main
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
new file mode 100644
index 0000000..1fe68bd
--- /dev/null
+++ b/deploy/docker-compose.yml
@@ -0,0 +1,24 @@
+name: rocketmq-studio
+
+services:
+  rocketmq-server:
+    build:
+      context: ../server
+      dockerfile: Dockerfile
+    image: rocketmq-server:latest
+    container_name: rocketmq-server
+    restart: unless-stopped
+    expose:
+      - "8888"
+
+  rocketmq-web:
+    build:
+      context: ../web
+      dockerfile: Dockerfile
+    image: rocketmq-web:latest
+    container_name: rocketmq-web
+    restart: unless-stopped
+    ports:
+      - "6789:80"
+    depends_on:
+      - rocketmq-server
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
new file mode 100644
index 0000000..165c0f9
--- /dev/null
+++ b/deploy/nginx.conf
@@ -0,0 +1,20 @@
+server {
+    listen 80;
+    server_name _;
+
+    location / {
+        root /usr/share/nginx/html;
+        index index.html;
+        try_files $uri $uri/ /index.html;
+    }
+
+    location /api/ {
+        resolver 10.89.0.1 valid=10s;
+        set $backend http://rocketmq-server:8888;
+        proxy_pass $backend;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}
diff --git a/docs/api-spec.md b/docs/api-spec.md
new file mode 100644
index 0000000..581e8d0
--- /dev/null
+++ b/docs/api-spec.md
@@ -0,0 +1,1696 @@
+<!--
+请更新 docs/api-spec.md 接口文档,基于以下数据源:
+1. 读取 web/src/mock/ 下所有 .ts 文件,提取每个文件的 interface/type 定义和 mock 数据的字段、类型、示例值
+2. 读取 web/src/pages/ 下所有页面组件,提取:
+   - Table columns 定义(title、dataIndex、render 函数)
+   - 表单字段(Form.Item 的 name、label、component、validation)
+   - 过滤/搜索参数
+   - CRUD 操作(按钮、modal、调用逻辑)
+   - 分页配置(pageSize、showSizeChanger)
+3. 读取 web/src/api/ 下所有 API 客户端文件,提取已有的 endpoint URL 和参数
+4. 读取 web/src/constants/ 下的枚举映射(如 TOPIC_TYPE_MAP、STATUS_MAP)
+5. 将以上信息整合为 GET+POST 风格 API 文档,每个接口包含:
+   - HTTP Method + Path
+   - Query Parameters / Path Parameters / Request Body(字段、类型、必填、说明)
+   - Response data 字段定义(字段、类型、说明、示例值)
+   - 枚举值在附录 A 统一列出
+6. 保持接口速查表与实际接口数量一致
+7. 如果前端新增了页面或 mock 数据,在对应模块章节下新增接口
+8. 如果前端删除了页面或字段,同步移除对应接口或字段
+-->
+
+# RocketMQ Studio API 接口规范
+
+> 本文档基于前端页面所需展示的全部数据和格式编写,用于指导后端 Java 代码开发。
+>
+> Base URL: `/api`
+> Content-Type: `application/json`
+> 认证方式: `Authorization: Bearer <token>`
+
+## 接口设计风格
+
+本项目采用 **GET + POST** 风格(非完整 RESTful),具体规则如下:
+
+- **GET** 用于所有查询/读取操作
+- **POST** 用于所有写入/操作类接口(创建、更新、删除、重启等)
+- URL 路径使用动词标识操作类型:`/create`、`/update`、`/delete`、`/toggle`、`/restart`、`/cleanup` 等
+- POST 操作如需定位特定资源,在 **Request Body** 中传递 `id` 或 `name`(不放在 URL 路径中)
+- GET 操作仍使用 Path Params 标识资源(如 `GET /api/topics/:name/routes`)
+
+## 接口速查
+
+| # | Method | Path | 说明 |
+|---|--------|------|------|
+| 1 | POST | `/api/auth/login` | 登录 |
+| 2 | POST | `/api/auth/logout` | 登出 |
+| 3 | GET | `/api/dashboard` | 面板数据(统计 + 集群概览) |
+| 4 | GET | `/api/instances` | 实例列表 |
+| 5 | POST | `/api/instances/create` | 创建实例 |
+| 6 | POST | `/api/instances/update` | 更新实例 |
+| 7 | POST | `/api/instances/delete` | 删除实例 |
+| 8 | GET | `/api/clusters` | 集群列表 |
+| 9 | GET | `/api/clusters/:id` | 集群详情 |
+| 10 | POST | `/api/clusters/config/update` | 更新集群配置 |
+| 11 | POST | `/api/clusters/:clusterId/brokers/:name/restart` | 重启 Broker |
+| 12 | POST | `/api/nameservers/create` | 创建 NameServer |
+| 13 | POST | `/api/nameservers/update` | 更新 NameServer |
+| 14 | POST | `/api/nameservers/restart` | 重启 NameServer |
+| 15 | POST | `/api/nameservers/upgrade` | 升级 NameServer |
+| 16 | POST | `/api/nameservers/delete` | 删除 NameServer |
+| 17 | POST | `/api/proxies/restart` | 重启 Proxy |
+| 18 | GET | `/api/k8s-certs` | K8s 证书列表 |
+| 19 | POST | `/api/k8s-certs/create` | 添加证书 |
+| 20 | POST | `/api/k8s-certs/update` | 更新证书 |
+| 21 | POST | `/api/k8s-certs/renew` | 续期证书 |
+| 22 | POST | `/api/k8s-certs/delete` | 删除证书 |
+| 23 | GET | `/api/topics` | Topic 列表 |
+| 24 | POST | `/api/topics/create` | 创建 Topic |
+| 25 | POST | `/api/topics/update` | 更新 Topic |
+| 26 | POST | `/api/topics/delete` | 删除 Topic |
+| 27 | GET | `/api/topics/:name/routes` | Topic 路由 |
+| 28 | GET | `/api/topics/:name/consumers` | Topic 消费者 |
+| 29 | POST | `/api/topics/send` | 发送消息到 Topic |
+| 30 | GET | `/api/groups` | 消费组列表 |
+| 31 | GET | `/api/groups/:name` | 消费组详情 |
+| 32 | GET | `/api/groups/:name/progress` | 消费进度 |
+| 33 | GET | `/api/groups/:name/subscriptions` | 订阅详情 |
+| 34 | POST | `/api/groups/create` | 创建消费组 |
+| 35 | POST | `/api/groups/delete` | 删除消费组 |
+| 36 | POST | `/api/groups/reset-offset` | 重置位点 |
+| 37 | POST | `/api/groups/import` | 导入配置 |
+| 38 | GET | `/api/groups/export` | 导出配置 |
+| 39 | GET | `/api/acl/rules` | ACL 规则列表 |
+| 40 | POST | `/api/acl/rules/create` | 创建 ACL 规则 |
+| 41 | POST | `/api/acl/rules/delete` | 删除 ACL 规则 |
+| 42 | GET | `/api/acl/users` | ACL 用户列表 |
+| 43 | POST | `/api/acl/users/create` | 创建 ACL 用户 |
+| 44 | POST | `/api/acl/users/delete` | 删除 ACL 用户 |
+| 45 | GET | `/api/messages` | 消息查询 |
+| 46 | GET | `/api/messages/:msgId/trace` | 消息轨迹 |
+| 47 | GET | `/api/dlq` | 死信队列列表 |
+| 48 | POST | `/api/dlq/resend` | 重发死信 |
+| 49 | GET | `/api/clients` | 客户端连接列表 |
+| 50 | GET | `/api/alert-rules` | 告警规则列表 |
+| 51 | POST | `/api/alert-rules/create` | 创建告警规则 |
+| 52 | POST | `/api/alert-rules/update` | 更新告警规则 |
+| 53 | POST | `/api/alert-rules/toggle` | 切换启用状态 |
+| 54 | POST | `/api/alert-rules/delete` | 删除告警规则 |
+| 55 | GET | `/api/system-alerts` | 系统告警列表 |
+| 56 | POST | `/api/system-alerts/acknowledge` | 确认告警 |
+| 57 | POST | `/api/system-alerts/clear-acknowledged` | 清除已确认告警 |
+| 58 | GET | `/api/audit-logs` | 审计日志列表 |
+| 59 | POST | `/api/audit-logs/cleanup` | 清理审计日志 |
+| 60 | GET | `/api/settings/general` | 获取通用设置 |
+| 61 | POST | `/api/settings/general/save` | 保存通用设置 |
+| 62 | GET | `/api/settings/datasources` | 数据源列表 |
+| 63 | POST | `/api/settings/datasources/create` | 创建数据源 |
+| 64 | POST | `/api/settings/datasources/update` | 更新数据源 |
+| 65 | POST | `/api/settings/datasources/delete` | 删除数据源 |
+| 66 | POST | `/api/settings/datasources/test` | 测试数据源连接 |
+| 67 | POST | `/api/ai/chat` | AI 对话(SSE) |
+| 68 | POST | `/api/ai/execute` | 执行 AI 指令 |
+| 69 | GET | `/api/ai/tools` | 可用工具列表 |
+| 70 | POST | `/api/metrics/query` | 查询监控指标数据 |
+
+## 通用响应格式
+
+所有接口统一返回以下 JSON 结构:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {}
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `code` | `int` | 200 表示成功,与 HTTP 状态码对齐 |
+| `message` | `string` | 提示信息 |
+| `data` | `any` | 响应数据 |
+
+**错误码规范:**
+
+| 错误码 | 含义 | 场景 |
+|--------|------|------|
+| `200` | 成功 | 正常响应 |
+| `400` | 参数错误 | 请求参数缺失或格式错误 |
+| `401` | 未认证 | Token 过期或缺失 |
+| `403` | 无权限 | 无权访问该资源 |
+| `404` | 不存在 | 资源未找到 |
+| `500` | 服务器异常 | 未预期的内部错误 |
+
+---
+
+## 1. 认证 Auth
+
+### 1.1 登录
+
+```
+POST /api/auth/login
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `username` | `string` | 是 | 用户名 |
+| `password` | `string` | 是 | 密码 |
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `token` | `string` | JWT Token |
+| `expiresIn` | `number` | 过期时间(秒) |
+| `user` | `object` | 用户信息 |
+| `user.username` | `string` | 用户名 |
+| `user.admin` | `boolean` | 是否管理员 |
+
+### 1.2 登出
+
+```
+POST /api/auth/logout
+```
+
+**Response `data`:** `null`
+
+---
+
+## 2. 监控面板 Dashboard
+
+### 2.1 获取面板数据(统计 + 集群概览)
+
+```
+GET /api/dashboard
+```
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `stats` | `DashboardStats` | 面板统计数据 |
+| `clusters` | `ClusterOverview[]` | 集群概览列表 |
+
+**DashboardStats 字段:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `totalClusters` | `number` | 集群总数 |
+| `healthyClusters` | `number` | 健康集群数 |
+| `totalBrokers` | `number` | Broker 总数(≥ 0) |
+| `totalProxies` | `number` | Proxy 总数(≥ 0) |
+| `totalNameServers` | `number` | NameServer 总数(≥ 0) |
+| `totalTopics` | `number` | Topic 总数 |
+| `totalConsumerGroups` | `number` | 消费组总数 |
+| `totalMessagesToday` | `number` | 今日消息总量 |
+| `messagesPerSecond` | `number` | 每秒消息数 |
+| `tpsIn` | `number` | 入站 TPS |
+| `tpsOut` | `number` | 出站 TPS |
+
+**ClusterOverview 字段:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 集群 ID |
+| `name` | `string` | 集群名称 |
+| `type` | `string` | 集群类型,枚举: `V4_DIRECT` / `V5_PROXY_LOCAL` / `V5_PROXY_CLUSTER` |
+| `status` | `string` | 状态: `healthy` / `warning` / `error` / `offline` |
+| `brokers` | `number` | Broker 数量(≥ 0) |
+| `proxies` | `number` | Proxy 数量(≥ 0,不会为负数) |
+| `topics` | `number` | Topic 数量 |
+| `groups` | `number` | 消费组数量 |
+| `tpsIn` | `number` | 入站 TPS |
+| `tpsOut` | `number` | 出站 TPS |
+| `version` | `string` | RocketMQ 版本号 |
+| `throughput` | `number[]` | TPS 趋势数据(12 个采样点) |
+
+**示例:**
+
+```json
+{
+  "stats": {
+    "totalClusters": 2,
+    "healthyClusters": 2,
+    "totalBrokers": 12,
+    "totalProxies": 5,
+    "totalNameServers": 5,
+    "totalTopics": 304,
+    "totalConsumerGroups": 152,
+    "totalMessagesToday": 18420000,
+    "messagesPerSecond": 85800,
+    "tpsIn": 85800,
+    "tpsOut": 222400
+  },
+  "clusters": [
+    {
+      "id": "cluster-prod",
+      "name": "rocketmq-prod",
+      "type": "V5_PROXY_CLUSTER",
+      "status": "healthy",
+      "brokers": 8,
+      "proxies": 3,
+      "topics": 256,
+      "groups": 128,
+      "tpsIn": 78000,
+      "tpsOut": 203600,
+      "version": "5.2.0",
+      "throughput": [62000, 68000, 74000, 71000, 78000, 82000, 79000, 85000, 81000, 78000, 76000, 80000]
+    }
+  ]
+}
+```
+
+---
+
+### 3.1 获取实例列表
+
+```
+GET /api/instances?type={type}&search={keyword}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `type` | `string` | 否 | 按类型过滤: `PROXY` / `DIRECT` |
+| `search` | `string` | 否 | 按名称或地址搜索 |
+
+**Response `data`:** `Instance[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 实例 ID |
+| `name` | `string` | 实例名称 |
+| `remark` | `string` | 备注 |
+| `type` | `string` | 接入类型: `PROXY` / `DIRECT` |
+| `endpoint` | `string` | 接入地址 |
+| `topicCount` | `number` | Topic 数量 |
+| `consumerGroupCount` | `number` | 消费组数量 |
+| `createdAt` | `string` | 创建时间 |
+| `updatedAt` | `string` | 更新时间 |
+
+### 3.2 创建实例
+
+```
+POST /api/instances/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | 实例名称 |
+| `type` | `string` | 是 | `PROXY` / `DIRECT` |
+| `endpoint` | `string` | 是 | 接入地址 |
+
+**Response `data`:** `Instance`
+
+### 3.3 更新实例
+
+```
+POST /api/instances/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 实例 ID |
+| `name` | `string` | 是 | 实例名称 |
+| `type` | `string` | 是 | `PROXY` / `DIRECT` |
+| `endpoint` | `string` | 是 | 接入地址 |
+
+**Response `data`:** `Instance`
+
+### 3.4 删除实例
+
+```
+POST /api/instances/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 实例 ID |
+
+**Response `data`:** `null`
+
+---
+
+## 4. 集群管理 Cluster / NameServer / Proxy
+
+> 前端在同一个页面展示,因此 API 合并在此章节。
+
+### 4.1 获取集群列表
+
+```
+GET /api/clusters
+```
+
+**Response `data`:** `ClusterInfo[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 集群 ID |
+| `name` | `string` | 集群名称 |
+| `nsClusterName` | `string` | NameServer 集群名 |
+| `type` | `string` | 集群类型,枚举: `V5_PROXY_CLUSTER` |
+| `endpoint` | `string` | NameServer 地址,如 `10.101.2.1:9876` |
+| `status` | `string` | 状态: `healthy` / `warning` / `error` / `offline` |
+| `version` | `string` | 版本号 |
+| `brokers` | `BrokerInfo[]` | Broker 节点列表 |
+| `proxies` | `ProxyInfo[]` | Proxy 节点列表 |
+| `nameServers` | `NameServerInfo[]` | NameServer 节点列表 |
+| `config` | `ClusterConfig` | 集群配置 |
+| `topicCount` | `number` | Topic 数量 |
+| `groupCount` | `number` | 消费组数量 |
+| `tpsHistory` | `number[]` | TPS 历史数据 |
+
+#### BrokerInfo
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | `string` | Broker 名称 |
+| `addr` | `string` | 地址,如 `10.101.2.11:10911` |
+| `version` | `string` | 版本号 |
+| `status` | `string` | 状态: `running` / `readonly` / `maintenance` |
+| `diskUsage` | `number` | 磁盘使用率(0-100) |
+| `tpsIn` | `number` | 入站 TPS |
+| `tpsOut` | `number` | 出站 TPS |
+
+#### ProxyInfo
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `addr` | `string` | 地址 |
+| `status` | `string` | 状态: `healthy` / `warning` / `error` / `offline` |
+| `connections` | `number` | 当前连接数 |
+| `grpcPort` | `number` | gRPC 端口 |
+| `remotingPort` | `number` | Remoting 端口 |
+
+#### NameServerInfo
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `addr` | `string` | 地址,如 `10.101.2.1:9876` |
+| `status` | `string` | 状态: `healthy` / `warning` / `error` / `offline` |
+
+#### ClusterConfig
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `writeQueueNums` | `number` | 写队列数 |
+| `readQueueNums` | `number` | 读队列数 |
+| `maxMessageSize` | `number` | 最大消息大小(字节) |
+| `msgTraceTopicName` | `string` | 消息轨迹 Topic 名 |
+| `autoCreateTopicEnable` | `boolean` | 是否自动创建 Topic |
+| `autoCreateSubscriptionGroup` | `boolean` | 是否自动创建订阅组 |
+| `deleteWhen` | `string` | 删除时间,如 `"04"` |
+| `fileReservedTime` | `number` | 文件保留时间(小时) |
+| `flushDiskType` | `string` | 刷盘方式: `ASYNC_FLUSH` / `SYNC_FLUSH` |
+| `brokerPermission` | `number` | Broker 权限值(6 = 读写) |
+
+### 4.2 获取集群详情
+
+```
+GET /api/clusters/:id
+```
+
+**Response `data`:** `ClusterInfo`(同 4.1 的单条记录)
+
+### 4.3 更新集群配置
+
+```
+POST /api/clusters/config/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 集群 ID |
+| `flushDiskType` | `string` | 是 | `ASYNC_FLUSH` / `SYNC_FLUSH` |
+| `autoCreateTopicEnable` | `boolean` | 是 | 自动创建 Topic |
+| `autoCreateSubscriptionGroup` | `boolean` | 是 | 自动创建订阅组 |
+| `maxMessageSize` | `number` | 是 | 最大消息大小(字节) |
+| `fileReservedTime` | `number` | 是 | 文件保留时间(小时,1-720) |
+| `writeQueueNums` | `number` | 是 | 写队列数(1-256) |
+| `readQueueNums` | `number` | 是 | 读队列数(1-256) |
+| `brokerPermission` | `number` | 是 | Broker 权限(0-7) |
+
+**Response `data`:** `null`
+
+### 4.4 重启 Broker
+
+```
+POST /api/clusters/:clusterId/brokers/:name/restart
+```
+
+**Path Parameters:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `clusterId` | `string` | 集群 ID |
+| `name` | `string` | Broker 名称 |
+
+**Response `data`:** `{ success: boolean, message: string }`
+
+### 4.5 创建 NameServer
+
+```
+POST /api/nameservers/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `clusterId` | `string` | 是 | 所属集群 ID |
+| `addr` | `string` | 是 | NameServer 地址,如 `10.0.1.1:9876` |
+
+**Response `data`:** `NameServerInfo`
+
+### 4.6 更新 NameServer
+
+```
+POST /api/nameservers/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `clusterId` | `string` | 是 | 所属集群 ID |
+| `addr` | `string` | 是 | 原地址 |
+| `newAddr` | `string` | 否 | 新地址(不传则不修改) |
+
+**Response `data`:** `NameServerInfo`
+
+### 4.7 重启 NameServer
+
+```
+POST /api/nameservers/restart
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `addr` | `string` | 是 | NameServer 地址 |
+
+**Response `data`:** `{ success: boolean }`
+
+### 4.8 升级 NameServer
+
+```
+POST /api/nameservers/upgrade
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `addr` | `string` | 是 | NameServer 地址 |
+
+**Response `data`:** `{ success: boolean }`
+
+### 4.9 删除 NameServer
+
+```
+POST /api/nameservers/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `addr` | `string` | 是 | NameServer 地址 |
+
+**Response `data`:** `{ success: boolean }`
+
+### 4.10 重启 Proxy
+
+```
+POST /api/proxies/restart
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `addr` | `string` | 是 | Proxy 地址 |
+
+**Response `data`:** `{ success: boolean }`
+
+### 4.11 获取 K8s 证书列表
+
+```
+GET /api/k8s-certs
+```
+
+**Response `data`:** `K8sCertInfo[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 证书 ID |
+| `name` | `string` | 证书名称 |
+| `namespace` | `string` | K8s 命名空间 |
+| `cluster` | `string` | 所属集群名称 |
+| `type` | `string` | 证书类型: `TLS` / `mTLS` / `ServiceAccount` |
+| `issuer` | `string` | 签发者 |
+| `notBefore` | `string` | 生效时间 (ISO 8601) |
+| `notAfter` | `string` | 过期时间 (ISO 8601) |
+| `status` | `string` | 状态: `valid` / `expiring` / `expired` |
+| `daysRemaining` | `number` | 剩余天数 |
+| `san` | `string[]` | Subject Alternative Name 列表 |
+
+### 4.12 添加 K8s 证书
+
+```
+POST /api/k8s-certs/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | 证书名称 |
+| `namespace` | `string` | 是 | K8s 命名空间 |
+| `cluster` | `string` | 是 | 所属集群 |
+| `type` | `string` | 是 | 证书类型: `TLS` / `mTLS` / `ServiceAccount` |
+| `issuer` | `string` | 否 | 签发者 |
+
+**Response `data`:** `K8sCertInfo`
+
+### 4.13 更新 K8s 证书
+
+```
+POST /api/k8s-certs/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 证书 ID |
+| `name` | `string` | 否 | 证书名称 |
+| `namespace` | `string` | 否 | K8s 命名空间 |
+| `cluster` | `string` | 否 | 所属集群 |
+| `type` | `string` | 否 | 证书类型 |
+| `issuer` | `string` | 否 | 签发者 |
+
+**Response `data`:** `K8sCertInfo`
+
+### 4.14 续期 K8s 证书
+
+```
+POST /api/k8s-certs/renew
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 证书 ID |
+
+**Response `data`:** `K8sCertInfo`
+
+### 4.15 删除 K8s 证书
+
+```
+POST /api/k8s-certs/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 证书 ID |
+
+**Response `data`:** `null`
+
+---
+
+## 5. Topic 管理
+
+### 5.1 获取 Topic 列表
+
+```
+GET /api/topics?clusterId={clusterId}&type={type}&search={keyword}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `clusterId` | `string` | 否 | 按集群过滤 |
+| `type` | `string` | 否 | 按类型过滤: `NORMAL` / `FIFO` / `DELAY` / `TRANSACTION` / `LITE` |
+| `search` | `string` | 否 | 按名称搜索 |
+
+**Response `data`:** `Topic[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | `string` | Topic 名称 |
+| `namespace` | `string` | 命名空间 |
+| `type` | `string` | 类型: `NORMAL` / `FIFO` / `DELAY` / `TRANSACTION` / `LITE` |
+| `clusterId` | `string` | 所属集群 ID |
+| `writeQueues` | `number` | 写队列数 |
+| `readQueues` | `number` | 读队列数 |
+| `perm` | `string` | 权限: `RW` / `RO` / `WO` |
+| `messageCount` | `number` | 消息总量 |
+| `tps` | `number` | 当前 TPS |
+| `consumerGroupCount` | `number` | 订阅消费组数 |
+| `remark` | `string` | 备注 |
+| `createdAt` | `string` | 创建时间 (ISO 8601) |
+| `updatedAt` | `string` | 更新时间 (ISO 8601) |
+
+### 5.2 创建 Topic
+
+```
+POST /api/topics/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | Topic 名称 |
+| `namespace` | `string` | 否 | 命名空间 |
+| `type` | `string` | 是 | 类型 |
+| `clusterId` | `string` | 是 | 所属集群 |
+| `writeQueues` | `number` | 是 | 写队列数 |
+| `readQueues` | `number` | 是 | 读队列数 |
+| `perm` | `string` | 否 | 权限,默认 `RW` |
+| `remark` | `string` | 否 | 备注 |
+
+**Response `data`:** `Topic`
+
+### 5.3 更新 Topic
+
+```
+POST /api/topics/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | Topic 名称(不可修改) |
+| `namespace` | `string` | 否 | 命名空间 |
+| `type` | `string` | 是 | 类型 |
+| `clusterId` | `string` | 是 | 所属集群 |
+| `writeQueues` | `number` | 是 | 写队列数 |
+| `readQueues` | `number` | 是 | 读队列数 |
+| `perm` | `string` | 否 | 权限,默认 `RW` |
+| `remark` | `string` | 否 | 备注 |
+
+**Response `data`:** `Topic`
+
+### 5.4 删除 Topic
+
+```
+POST /api/topics/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | Topic 名称 |
+
+**Response `data`:** `null`
+
+### 5.5 获取 Topic 路由信息
+
+```
+GET /api/topics/:name/routes
+```
+
+**Response `data`:** `BrokerRoute[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `brokerName` | `string` | Broker 名称 |
+| `brokerAddr` | `string` | Broker 地址 |
+| `writeQueues` | `number` | 写队列数 |
+| `readQueues` | `number` | 读队列数 |
+| `perm` | `string` | 权限 |
+
+### 5.6 获取 Topic 消费者列表
+
+```
+GET /api/topics/:name/consumers
+```
+
+**Response `data`:** `TopicConsumerInfo[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `group` | `string` | 消费组名称 |
+| `consumeType` | `string` | 消费类型: `CLUSTERING` / `BROADCASTING` |
+| `messageModel` | `string` | 消费模式描述 |
+| `consumeTps` | `number` | 消费 TPS |
+| `diffTotal` | `number` | 堆积消息数 |
+
+### 5.7 发送消息到 Topic
+
+```
+POST /api/topics/send
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `topic` | `string` | 是 | Topic 名称 |
+| `tag` | `string` | 否 | 消息 Tag |
+| `key` | `string` | 否 | 消息 Key(用于消息查询) |
+| `body` | `string` | 是 | 消息体内容 |
+| `properties` | `Record<string, string>` | 否 | 消息自定义属性键值对 |
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `msgId` | `string` | 消息 ID |
+| `sendTime` | `string` | 发送时间 (ISO 8601) |
+| `offsetMsgId` | `string` | 含偏移量的消息 ID |
+
+**示例:**
+
+```json
+// Request
+{
+  "topic": "order-create",
+  "tag": "order",
+  "key": "ORDER-20260708-001",
+  "body": "{\"orderId\":\"20260708001\",\"amount\":299.00,\"status\":\"CREATED\"}",
+  "properties": {
+    "source": "rocketmq-studio",
+    "env": "test"
+  }
+}
+
+// Response
+{
+  "msgId": "7F000001234567890000",
+  "sendTime": "2026-07-08T10:30:45.123Z",
+  "offsetMsgId": "7F000001234567890000-0:0:0:0"
+}
+```
+
+---
+
+## 6. 消费组管理 Consumer Group
+
+### 6.1 获取消费组列表
+
+```
+GET /api/groups?clusterId={clusterId}&search={keyword}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `clusterId` | `string` | 否 | 按集群过滤 |
+| `search` | `string` | 否 | 按名称搜索 |
+
+**Response `data`:** `ConsumerGroup[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | `string` | 消费组名称 |
+| `namespace` | `string` | 命名空间 |
+| `clusterId` | `string` | 所属集群 ID |
+| `subscriptionMode` | `string` | 订阅模式: `Push` / `Pop` |
+| `consumeType` | `string` | 消费类型: `CLUSTERING` / `BROADCASTING` |
+| `onlineInstances` | `number` | 在线实例数 |
+| `totalLag` | `number` | 总堆积消息数 |
+| `subscribedTopics` | `string[]` | 订阅的 Topic 列表 |
+| `subscriptionDataType` | `string` | 订阅数据类型: `NORMAL` / `FIFO` / `DELAY` / `TRANSACTION` |
+| `deliveryOrderType` | `string?` | 顺序类型(FIFO 时): `PARTITON_ORDER` / `MESSAGES_ORDER` |
+| `retryMaxTimes` | `number` | 最大重试次数 |
+| `delaySeconds` | `number` | 延迟秒数 |
+| `createdAt` | `string` | 创建时间 |
+| `updatedAt` | `string` | 更新时间 |
+
+### 6.2 获取消费组详情
+
+```
+GET /api/groups/:name
+```
+
+**Response `data`:** `ConsumerGroup`(含 `instances` 字段)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `instances` | `ConsumerInstance[]` | 消费者实例列表 |
+
+#### ConsumerInstance
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `clientId` | `string` | 客户端 ID |
+| `protocol` | `string` | 协议: `REMOTING` / `GRPC` |
+| `address` | `string` | 客户端地址 |
+| `subscribedTopics` | `string[]` | 订阅的 Topic |
+| `lastHeartbeat` | `string` | 最后心跳时间 |
+| `topicLag` | `Record<string, number>` | 各 Topic 堆积数 `{ "topic-name": lag_count }` |
+
+### 6.3 获取消费进度
+
+```
+GET /api/groups/:name/progress
+```
+
+**Response `data`:** `QueueProgress[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `broker` | `string` | Broker 名称 |
+| `queueId` | `number` | 队列 ID |
+| `brokerOffset` | `number` | Broker 端 offset |
+| `consumerOffset` | `number` | 消费端 offset |
+| `diffTotal` | `number` | 堆积差值 |
+
+### 6.4 获取订阅详情
+
+```
+GET /api/groups/:name/subscriptions
+```
+
+**Response `data`:** `SubscriptionEntry[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `topic` | `string` | Topic 名称 |
+| `expression` | `string` | 过滤表达式 |
+| `type` | `string` | 数据类型: `NORMAL` / `FIFO` / `DELAY` / `TRANSACTION` |
+| `filterMode` | `string` | 过滤模式: `Tag 过滤` / `SQL92 过滤` / `全量` |
+| `consistency` | `string` | 一致性: `一致` / `不一致` |
+
+### 6.5 创建消费组
+
+```
+POST /api/groups/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | 消费组名称 |
+| `namespace` | `string` | 否 | 命名空间 |
+| `clusterId` | `string` | 是 | 所属集群 |
+| `subscriptionMode` | `string` | 是 | `Push` / `Pop` |
+| `consumeType` | `string` | 是 | `CLUSTERING` / `BROADCASTING` |
+| `subscribedTopics` | `string[]` | 否 | 订阅 Topic |
+| `retryMaxTimes` | `number` | 否 | 最大重试次数 |
+
+**Response `data`:** `ConsumerGroup`
+
+### 6.6 删除消费组
+
+```
+POST /api/groups/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | 消费组名称 |
+
+**Response `data`:** `null`
+
+### 6.7 重置消费位点
+
+```
+POST /api/groups/reset-offset
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | 消费组名称 |
+| `timestamp` | `string` | 是 | 重置到指定时间 (ISO 8601) |
+| `topic` | `string` | 否 | 指定 Topic,不传则全部重置 |
+
+**Response `data`:** `null`
+
+### 6.8 导入消费组配置
+
+```
+POST /api/groups/import
+```
+
+**Request Body:** `multipart/form-data`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `file` | `File` | JSON 配置文件 |
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `imported` | `number` | 成功导入数量 |
+| `failed` | `number` | 失败数量 |
+| `errors` | `string[]` | 错误信息列表 |
+
+### 6.9 导出消费组配置
+
+```
+GET /api/groups/export?names={name1,name2}
+```
+
+**Response:** `Content-Type: application/json`, `Content-Disposition: attachment`
+
+---
+
+## 7. ACL 管理
+
+### 7.1 获取 ACL 规则列表
+
+```
+GET /api/acl/rules?clusterId={clusterId}&principal={principal}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `clusterId` | `string` | 否 | 按集群过滤 |
+| `principal` | `string` | 否 | 按用户过滤 |
+
+**Response `data`:** `AclRule[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 规则 ID |
+| `principal` | `string` | 主体用户名 |
+| `resource` | `string` | 资源名称/模式 |
+| `resourceType` | `string` | 资源类型: `Topic` / `Group` / `Cluster` |
+| `resourcePattern` | `string` | 匹配模式: `LITERAL` / `PREFIX` |
+| `actions` | `string[]` | 操作列表: `PUB` / `SUB` / `ALL` |
+| `decision` | `string` | 决策: `ALLOW` / `DENY` |
+| `scope` | `string` | 作用域: `cluster` / `namespace` |
+| `aclVersion` | `string` | ACL 版本: `1.0` / `2.0` |
+| `createdAt` | `string` | 创建时间 (ISO 8601) |
+
+### 7.2 创建 ACL 规则
+
+```
+POST /api/acl/rules/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `principal` | `string` | 是 | 主体 |
+| `resource` | `string` | 是 | 资源 |
+| `resourceType` | `string` | 是 | 资源类型 |
+| `resourcePattern` | `string` | 是 | 匹配模式 |
+| `actions` | `string[]` | 是 | 操作列表 |
+| `decision` | `string` | 是 | ALLOW / DENY |
+| `scope` | `string` | 否 | 作用域 |
+
+**Response `data`:** `AclRule`
+
+### 7.3 删除 ACL 规则
+
+```
+POST /api/acl/rules/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 规则 ID |
+
+**Response `data`:** `null`
+
+### 7.4 获取 ACL 用户列表
+
+```
+GET /api/acl/users
+```
+
+**Response `data`:** `AclUser[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 用户 ID |
+| `username` | `string` | 用户名 |
+| `accessKey` | `string` | AccessKey(脱敏显示) |
+| `secretKey` | `string` | SecretKey(脱敏显示) |
+| `admin` | `boolean` | 是否管理员 |
+| `clusters` | `string[]` | 授权集群列表 |
+| `createdAt` | `string` | 创建时间 |
+
+### 7.5 创建 ACL 用户
+
+```
+POST /api/acl/users/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `username` | `string` | 是 | 用户名 |
+| `admin` | `boolean` | 否 | 是否管理员 |
+| `clusters` | `string[]` | 否 | 授权集群 |
+
+**Response `data`:** `AclUser`(含生成的 accessKey/secretKey)
+
+### 7.6 删除 ACL 用户
+
+```
+POST /api/acl/users/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 用户 ID |
+
+**Response `data`:** `null`
+
+---
+
+## 8. 消息查询 Message
+
+### 8.1 查询消息列表
+
+```
+GET /api/messages?topic={topic}&msgId={msgId}&key={key}&startTime={startTime}&endTime={endTime}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `topic` | `string` | 否 | Topic 名称 |
+| `msgId` | `string` | 否 | 消息 ID(精确查询) |
+| `key` | `string` | 否 | 消息 Key |
+| `startTime` | `string` | 否 | 开始时间 (ISO 8601) |
+| `endTime` | `string` | 否 | 结束时间 (ISO 8601) |
+
+**Response `data`:** `MessageRecord[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `msgId` | `string` | 消息 ID |
+| `topic` | `string` | Topic 名称 |
+| `tag` | `string` | 消息 Tag |
+| `key` | `string` | 消息 Key |
+| `body` | `string` | 消息体(JSON 字符串) |
+| `storeTime` | `string` | 存储时间 (ISO 8601) |
+| `bornHost` | `string` | 发送方地址 |
+| `storeHost` | `string` | 存储 Broker 地址 |
+| `properties` | `Record<string, string>` | 消息属性键值对 |
+| `size` | `number` | 消息大小(字节) |
+
+### 8.2 获取消息轨迹
+
+```
+GET /api/messages/:msgId/trace
+```
+
+**Response `data`:** `TraceRecord`
+
+```json
+{
+  "nodes": [
+    {
+      "title": "Producer 发送",
+      "timestamp": "2026-07-01T10:23:45.100Z",
+      "status": "finish",
+      "costTime": 3,
+      "description": "order-service (10.0.1.12:54321) → broker-hz-01"
+    },
+    {
+      "title": "Broker 存储",
+      "timestamp": "2026-07-01T10:23:45.115Z",
+      "status": "finish",
+      "costTime": 8,
+      "description": "broker-hz-01 (10.0.2.3:10911) CommitLog 写入成功"
+    },
+    {
+      "title": "Consumer 消费",
+      "timestamp": "2026-07-01T10:23:45.230Z",
+      "status": "finish",
+      "costTime": 107,
+      "description": "cg-order-processor → 消费成功 (23ms)"
+    }
+  ],
+  "consumerStatus": [
+    {
+      "group": "cg-order-processor",
+      "deliveryStatus": "success",
+      "consumeTime": "2026-07-01T10:23:45.230Z",
+      "retryCount": 0
+    }
+  ]
+}
+```
+
+#### TraceNode
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `title` | `string` | 节点标题: `Producer 发送` / `Broker 存储` / `Consumer 消费` |
+| `timestamp` | `string` | 时间戳 (ISO 8601) |
+| `status` | `string` | 状态: `finish` / `process` / `wait` |
+| `costTime` | `number` | 耗时(毫秒) |
+| `description` | `string` | 详细描述 |
+
+#### ConsumerStatus
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `group` | `string` | 消费组名称 |
+| `deliveryStatus` | `string` | 投递状态: `success` / `failed` / `pending` |
+| `consumeTime` | `string` | 消费时间(`-` 表示未消费) |
+| `retryCount` | `number` | 重试次数 |
+
+---
+
+## 9. 死信队列 Dead Letter Queue
+
+### 9.1 获取 DLQ 列表
+
+```
+GET /api/dlq?clusterId={clusterId}
+```
+
+**Response `data`:** `DLQGroup[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `groupName` | `string` | 消费组名称 |
+| `dlqTopic` | `string` | 死信 Topic 名称(格式: `%DLQ%{groupName}`) |
+| `messageCount` | `number` | 死信消息数量 |
+| `lastEnqueueTime` | `string` | 最后入队时间 (ISO 8601) |
+| `retryCount` | `number` | 已重试次数 |
+| `status` | `string` | 状态: `active` / `empty` |
+
+### 9.2 重发死信消息
+
+```
+POST /api/dlq/resend
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `groupName` | `string` | 是 | 消费组名称 |
+| `startTime` | `string` | 是 | 重投时间范围起始 (ISO 8601) |
+| `endTime` | `string` | 是 | 重投时间范围结束 (ISO 8601) |
+| `targetTopic` | `string` | 否 | 目标 Topic,不传则重投回原 Topic |
+
+**Response `data`:** `null`
+
+---
+
+## 10. 客户端连接 Clients
+
+### 10.1 获取客户端连接列表
+
+```
+GET /api/clients?clusterId={clusterId}&type={type}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `clusterId` | `string` | 否 | 按集群过滤 |
+| `type` | `string` | 否 | 按类型过滤: `Producer` / `Consumer` |
+
+**Response `data`:** `ClientConnection[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `clientId` | `string` | 客户端 ID |
+| `type` | `string` | 类型: `Producer` / `Consumer` |
+| `groupOrTopic` | `string` | 消费组名或 Topic 名 |
+| `protocol` | `string` | 协议: `gRPC` / `Remoting` |
+| `address` | `string` | 客户端地址 |
+| `language` | `string` | 客户端语言: `Java` / `Go` / `Python` / `Rust` / `C++` / `C#` / `Node.js` / `PHP` |
+| `version` | `string` | SDK 版本号 |
+| `connectedAt` | `string` | 连接时间 |
+| `clusterName` | `string` | 所属集群名称(显示在第一列) |
+
+---
+
+## 11. 告警规则 Alert Rules
+
+### 11.1 获取告警规则列表
+
+```
+GET /api/alert-rules
+```
+
+**Response `data`:** `AlertRule[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 规则 ID |
+| `name` | `string` | 规则名称 |
+| `metric` | `string` | 监控指标: `磁盘使用率` / `消费堆积量` / `TPS 异常` / `Broker 离线` / `Proxy 连接数` |
+| `operator` | `string` | 比较运算符: `>` / `<` / `>=` / `<=` |
+| `threshold` | `number` | 阈值 |
+| `thresholdUnit` | `string` | 单位: `%` / `条` / `TPS` / `个` |
+| `duration` | `string` | 持续时间: `1分钟` / `5分钟` / `15分钟` / `30分钟` |
+| `channels` | `string[]` | 通知渠道: `dingtalk` / `email` / `sms` |
+| `enabled` | `boolean` | 是否启用 |
+| `lastTriggered` | `string \| null` | 最后触发时间,null 表示未触发 |
+| `description` | `string` | 规则描述 |
+
+### 11.2 创建告警规则
+
+```
+POST /api/alert-rules/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | 规则名称 |
+| `metric` | `string` | 是 | 监控指标 |
+| `operator` | `string` | 是 | 运算符 |
+| `threshold` | `number` | 是 | 阈值 |
+| `duration` | `string` | 是 | 持续时间 |
+| `channels` | `string[]` | 是 | 通知渠道 |
+| `description` | `string` | 否 | 描述 |
+
+**Response `data`:** `AlertRule`
+
+### 11.3 更新告警规则
+
+```
+POST /api/alert-rules/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 规则 ID |
+| `name` | `string` | 是 | 规则名称 |
+| `metric` | `string` | 是 | 监控指标 |
+| `operator` | `string` | 是 | 运算符 |
+| `threshold` | `number` | 是 | 阈值 |
+| `duration` | `string` | 是 | 持续时间 |
+| `channels` | `string[]` | 是 | 通知渠道 |
+| `description` | `string` | 否 | 描述 |
+
+**Response `data`:** `AlertRule`
+
+### 11.4 切换告警规则启用状态
+
+```
+POST /api/alert-rules/toggle
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 规则 ID |
+| `enabled` | `boolean` | 是 | 启用/禁用 |
+
+**Response `data`:** `AlertRule`
+
+### 11.5 删除告警规则
+
+```
+POST /api/alert-rules/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 规则 ID |
+
+**Response `data`:** `null`
+
+---
+
+## 12. 系统告警 System Alerts
+
+### 12.1 获取系统告警列表
+
+```
+GET /api/system-alerts?level={level}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `level` | `string` | 否 | 按级别过滤: `error` / `warning` / `info` |
+
+**Response `data`:** `SystemAlert[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 告警 ID |
+| `level` | `string` | 级别: `error`(严重) / `warning`(警告) / `info`(信息) |
+| `title` | `string` | 告警标题 |
+| `description` | `string` | 告警详情 |
+| `time` | `string` | 时间(短格式: `HH:mm`) |
+| `acknowledged` | `boolean` | 是否已确认 |
+
+### 12.2 确认告警
+
+```
+POST /api/system-alerts/acknowledge
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 告警 ID |
+
+**Response `data`:** `SystemAlert`
+
+### 12.3 清除已确认告警
+
+```
+POST /api/system-alerts/clear-acknowledged
+```
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `cleared` | `number` | 清除数量 |
+
+---
+
+## 13. 审计日志 Audit
+
+### 13.1 获取审计日志列表
+
+```
+GET /api/audit-logs?page={page}&pageSize={pageSize}&search={search}&operationType={type}&startDate={start}&endDate={end}&result={result}
+```
+
+**Query Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `page` | `number` | 否 | 页码,默认 1 |
+| `pageSize` | `number` | 否 | 每页条数,默认 20 |
+| `search` | `string` | 否 | 搜索(匹配 operator / target) |
+| `operationType` | `string` | 否 | 操作类型过滤 |
+| `startDate` | `string` | 否 | 开始日期 (YYYY-MM-DD) |
+| `endDate` | `string` | 否 | 结束日期 (YYYY-MM-DD) |
+| `result` | `string` | 否 | 结果过滤: `success` / `failure` |
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `records` | `AuditRecord[]` | 记录列表 |
+| `total` | `number` | 总条数 |
+
+#### AuditRecord
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | `string` | 记录 ID |
+| `timestamp` | `string` | 操作时间(`YYYY-MM-DD HH:mm:ss`) |
+| `operator` | `string` | 操作人(如 `admin`, `ops-zhang`, `system`) |
+| `operationType` | `string` | 操作类型: `创建Topic` / `删除Topic` / `修改配置` / `重置位点` / `ACL变更` / `重启Broker` / `删除消费组` |
+| `target` | `string` | 操作对象 |
+| `detail` | `string` | 详细描述 |
+| `ipAddress` | `string` | 操作 IP 地址 |
+| `result` | `string` | 结果: `success` / `failure` |
+
+### 13.2 清理审计日志
+
+```
+POST /api/audit-logs/cleanup
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `beforeDays` | `number` | 是 | 清理多少天之前的日志(1-365) |
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `deleted` | `number` | 清理数量 |
+
+---
+
+## 14. 系统设置 Settings
+
+### 14.1 获取通用设置
+
+```
+GET /api/settings/general
+```
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `theme` | `string` | 主题: `light` / `dark` / `system` |
+| `compact` | `boolean` | 紧凑模式 |
+| `desktopNotify` | `boolean` | 桌面通知 |
+| `notifySound` | `boolean` | 通知声音 |
+| `sessionTimeout` | `number` | 会话超时(分钟,5-1440) |
+| `requireLogin` | `boolean` | 是否需要登录 |
+| `llmProvider` | `string` | LLM 提供商: `openai` / `azure` / `ollama` / `qwen` |
+| `apiKey` | `string` | API Key |
+| `model` | `string` | 模型名称 |
+| `baseUrl` | `string` | Base URL |
+
+### 14.2 保存通用设置
+
+```
+POST /api/settings/general/save
+```
+
+**Request Body:** 同 14.1 响应格式
+
+**Response `data`:** `null`
+
+### 14.3 获取数据源列表
+
+```
+GET /api/settings/datasources
+```
+
+**Response `data`:** `DataSource[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `key` | `string` | 数据源 ID |
+| `name` | `string` | 名称 |
+| `type` | `string` | 类型: `Prometheus` / `VictoriaMetrics` / `Thanos` |
+| `url` | `string` | 连接 URL |
+| `auth` | `string` | 认证方式: `None` / `Basic Auth` / `Bearer Token` |
+| `status` | `string` | 状态: `healthy` / `error` |
+
+### 14.4 创建数据源
+
+```
+POST /api/settings/datasources/create
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `name` | `string` | 是 | 名称 |
+| `type` | `string` | 是 | 类型 |
+| `url` | `string` | 是 | URL |
+| `auth` | `string` | 否 | 认证方式,默认 `None` |
+
+**Response `data`:** `DataSource`
+
+### 14.5 更新数据源
+
+```
+POST /api/settings/datasources/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `key` | `string` | 是 | 数据源 ID |
+| `name` | `string` | 是 | 名称 |
+| `type` | `string` | 是 | 类型 |
+| `url` | `string` | 是 | URL |
+| `auth` | `string` | 否 | 认证方式,默认 `None` |
+
+**Response `data`:** `DataSource`
+
+### 14.6 删除数据源
+
+```
+POST /api/settings/datasources/delete
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `key` | `string` | 是 | 数据源 ID |
+
+**Response `data`:** `null`
+
+### 14.7 测试数据源连接
+
+```
+POST /api/settings/datasources/test
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `url` | `string` | 是 | 连接 URL |
+| `type` | `string` | 是 | 类型 |
+| `auth` | `string` | 否 | 认证方式 |
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `success` | `boolean` | 是否连接成功 |
+| `message` | `string?` | 错误信息 |
+
+---
+
+## 15. AI 交互
+
+### 15.1 发送 AI 消息
+
+```
+POST /api/ai/chat
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `message` | `string` | 是 | 用户消息 |
+| `mode` | `string` | 否 | 模式: `query` / `diagnose` / `manage` / `chat` |
+| `model` | `string` | 否 | 模型名称 |
+| `conversationId` | `string` | 否 | 会话 ID(续接对话) |
+
+**Response:** `text/event-stream`(SSE 流式返回)
+
+### 15.2 执行 AI 指令
+
+```
+POST /api/ai/execute
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `command` | `string` | AI 生成的可执行指令 |
+
+**Response `data`:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `success` | `boolean` | 执行结果 |
+| `result` | `string` | 执行输出 |
+
+### 15.3 获取可用工具列表
+
+```
+GET /api/ai/tools
+```
+
+**Response `data`:** `Tool[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | `string` | 工具名称 |
+| `description` | `string` | 工具描述 |
+| `parameters` | `object` | 参数 Schema |
+
+---
+
+## 16. 监控指标 Metrics
+
+### 16.1 查询监控指标数据
+
+```
+POST /api/metrics/query
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `metric` | `string` | 是 | 指标名称(如 `tps_in`、`tps_out`、`message_count`、`disk_usage`) |
+| `start` | `number` | 是 | 起始时间(Unix 时间戳,秒) |
+| `end` | `number` | 是 | 结束时间(Unix 时间戳,秒) |
+| `step` | `string` | 否 | 采样步长(如 `"60s"`、`"5m"`、`"1h"`) |
+
+**Response `data`:** `MetricsResult`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `metric` | `string` | 指标名称 |
+| `values` | `[number, number][]` | 数据点数组,每项为 `[timestamp, value]` |
+
+---
+
+## 附录 A:枚举值速查
+
+| 枚举 | 值 |
+|------|---|
+| **集群状态** | `healthy`, `warning`, `error`, `offline` |
+| **Broker 状态** | `running`, `readonly`, `maintenance` |
+| **节点状态** | `healthy`, `warning`, `error`, `offline` |
+| **Topic 类型** | `NORMAL`, `FIFO`, `DELAY`, `TRANSACTION`, `LITE` |
+| **Topic 权限** | `RW`, `RO`, `WO` |
+| **消费类型** | `CLUSTERING`, `BROADCASTING` |
+| **订阅模式** | `Push`, `Pop` |
+| **协议** | `gRPC`, `Remoting` |
+| **客户端语言** | `Java`, `Go`, `Python`, `Rust`, `C++`, `C#`, `Node.js`, `PHP` |
+| **ACL 资源类型** | `Topic`, `Group`, `Cluster` |
+| **ACL 匹配模式** | `LITERAL`, `PREFIX` |
+| **ACL 操作** | `PUB`, `SUB`, `ALL` |
+| **ACL 决策** | `ALLOW`, `DENY` |
+| **告警级别** | `error`, `warning`, `info` |
+| **审计结果** | `success`, `failure` |
+| **投递状态** | `success`, `failed`, `pending` |
+| **证书状态** | `valid`, `expiring`, `expired` |
+| **证书类型** | `TLS`, `mTLS`, `ServiceAccount` |
+| **刷盘方式** | `ASYNC_FLUSH`, `SYNC_FLUSH` |
+| **集群类型** | `V4_DIRECT`, `V5_PROXY_LOCAL`, `V5_PROXY_CLUSTER` |
+| **顺序类型** | `PARTITON_ORDER`, `MESSAGES_ORDER` |
+| **通知渠道** | `dingtalk`, `email`, `sms` |
+| **LLM 提供商** | `openai`, `azure`, `ollama`, `qwen` |
+| **数据源类型** | `Prometheus`, `VictoriaMetrics`, `Thanos` |


(rocketmq-dashboard) 05/09: feat: implement backend common module and auth

Posted by li...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git

commit 49bc441957ab6d82af7c16952d699c364b220524
Author: lizhimins <70...@qq.com>
AuthorDate: Sun Jul 5 15:34:00 2026 +0800

    feat: implement backend common module and auth
    
    - Add common domain classes (Result, PageResult, BaseEntity)
    - Add 16 business enums (ClusterStatus, TopicType, etc.)
    - Implement global exception handler
    - Configure CORS and web MVC
    - Implement authentication (AuthController, AuthService)
    - Add auth unit tests
---
 .../com/rocketmq/studio/auth/AuthController.java   |  44 ++++++++
 .../java/com/rocketmq/studio/auth/AuthService.java |  60 +++++++++++
 .../java/com/rocketmq/studio/auth/LoginDTO.java    |  26 +++++
 .../java/com/rocketmq/studio/auth/LoginVO.java     |  42 ++++++++
 .../com/rocketmq/studio/auth/SecurityConfig.java   |  25 +++++
 .../rocketmq/studio/common/config/CorsConfig.java  |  33 ++++++
 .../rocketmq/studio/common/config/WebConfig.java   |  25 +++++
 .../rocketmq/studio/common/domain/BaseEntity.java  |  27 +++++
 .../rocketmq/studio/common/domain/PageResult.java  |  47 +++++++++
 .../com/rocketmq/studio/common/domain/Result.java  |  47 +++++++++
 .../studio/common/domain/enums/AlertLevel.java     |  22 ++++
 .../studio/common/domain/enums/BrokerStatus.java   |  22 ++++
 .../studio/common/domain/enums/CertStatus.java     |  22 ++++
 .../studio/common/domain/enums/CertType.java       |  22 ++++
 .../studio/common/domain/enums/ClientLanguage.java |  22 ++++
 .../studio/common/domain/enums/ClientType.java     |  22 ++++
 .../studio/common/domain/enums/ClusterStatus.java  |  22 ++++
 .../studio/common/domain/enums/ClusterType.java    |  22 ++++
 .../studio/common/domain/enums/ConsumeType.java    |  22 ++++
 .../studio/common/domain/enums/DeliveryStatus.java |  22 ++++
 .../studio/common/domain/enums/FlushDiskType.java  |  22 ++++
 .../studio/common/domain/enums/InstanceType.java   |  22 ++++
 .../studio/common/domain/enums/Protocol.java       |  22 ++++
 .../common/domain/enums/SubscriptionMode.java      |  22 ++++
 .../studio/common/domain/enums/TopicPerm.java      |  22 ++++
 .../studio/common/domain/enums/TopicType.java      |  22 ++++
 .../studio/common/exception/BusinessException.java |  29 ++++++
 .../common/exception/GlobalExceptionHandler.java   |  45 ++++++++
 .../rocketmq/studio/auth/AuthControllerTest.java   | 115 +++++++++++++++++++++
 .../com/rocketmq/studio/auth/AuthServiceTest.java  | 115 +++++++++++++++++++++
 30 files changed, 1032 insertions(+)

diff --git a/server/src/main/java/com/rocketmq/studio/auth/AuthController.java b/server/src/main/java/com/rocketmq/studio/auth/AuthController.java
new file mode 100644
index 0000000..8b4f6f1
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/auth/AuthController.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rocketmq.studio.auth;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+
+    @PostMapping("/login")
+    public Result<LoginVO> login(@RequestBody LoginDTO request) {
+        return Result.ok(authService.login(request));
+    }
+
+    @PostMapping("/logout")
+    public Result<Void> logout() {
+        authService.logout();
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/auth/AuthService.java b/server/src/main/java/com/rocketmq/studio/auth/AuthService.java
new file mode 100644
index 0000000..0591cef
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/auth/AuthService.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.rocketmq.studio.auth;
+
+import com.rocketmq.studio.common.exception.BusinessException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+
+@Slf4j
+@Service
+public class AuthService {
+
+    public LoginVO login(LoginDTO request) {
+        log.info("Login attempt for user: {}", request.getUsername());
+
+        if (request.getUsername() == null || request.getUsername().isBlank()) {
+            throw new BusinessException(400, "Username is required");
+        }
+        if (request.getPassword() == null || request.getPassword().isBlank()) {
+            throw new BusinessException(400, "Password is required");
+        }
+
+        // Mock authentication — accept any non-empty credentials
+        String token = "mock-jwt-" + UUID.randomUUID();
+        boolean isAdmin = "admin".equals(request.getUsername());
+
+        LoginVO response = LoginVO.builder()
+                .token(token)
+                .expiresIn(86400)
+                .user(LoginVO.UserInfo.builder()
+                        .username(request.getUsername())
+                        .admin(isAdmin)
+                        .build())
+                .build();
+
+        log.info("User {} logged in successfully, admin={}", request.getUsername(), isAdmin);
+        return response;
+    }
+
+    public void logout() {
+        log.info("User logged out");
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/auth/LoginDTO.java b/server/src/main/java/com/rocketmq/studio/auth/LoginDTO.java
new file mode 100644
index 0000000..a24238f
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/auth/LoginDTO.java
@@ -0,0 +1,26 @@
+/*
+ * 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 com.rocketmq.studio.auth;
+
+import lombok.Data;
+
+@Data
+public class LoginDTO {
+    private String username;
+    private String password;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/auth/LoginVO.java b/server/src/main/java/com/rocketmq/studio/auth/LoginVO.java
new file mode 100644
index 0000000..2a826f5
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/auth/LoginVO.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.rocketmq.studio.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoginVO {
+    private String token;
+    private int expiresIn;
+    private UserInfo user;
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class UserInfo {
+        private String username;
+        private boolean admin;
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/auth/SecurityConfig.java b/server/src/main/java/com/rocketmq/studio/auth/SecurityConfig.java
new file mode 100644
index 0000000..7784098
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/auth/SecurityConfig.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.auth;
+
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SecurityConfig {
+    // TODO: Add Spring Security configuration when Spring Security dependency is added
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/config/CorsConfig.java b/server/src/main/java/com/rocketmq/studio/common/config/CorsConfig.java
new file mode 100644
index 0000000..fd14f2f
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/config/CorsConfig.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.rocketmq.studio.common.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOrigins("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+                .allowedHeaders("*");
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/config/WebConfig.java b/server/src/main/java/com/rocketmq/studio/common/config/WebConfig.java
new file mode 100644
index 0000000..951171f
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/config/WebConfig.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.common.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+    // TODO: Add web configuration as needed
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/BaseEntity.java b/server/src/main/java/com/rocketmq/studio/common/domain/BaseEntity.java
new file mode 100644
index 0000000..50144c5
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/BaseEntity.java
@@ -0,0 +1,27 @@
+/*
+ * 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 com.rocketmq.studio.common.domain;
+
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+public abstract class BaseEntity {
+    private String id;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/PageResult.java b/server/src/main/java/com/rocketmq/studio/common/domain/PageResult.java
new file mode 100644
index 0000000..0d6b1e4
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/PageResult.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.common.domain;
+
+import java.util.Collections;
+import java.util.List;
+
+public class PageResult<T> {
+    private List<T> items;
+    private long total;
+    private int page;
+    private int size;
+
+    private PageResult() {}
+
+    public static <T> PageResult<T> of(List<T> items, long total, int page, int size) {
+        PageResult<T> result = new PageResult<>();
+        result.items = items;
+        result.total = total;
+        result.page = page;
+        result.size = size;
+        return result;
+    }
+
+    public static <T> PageResult<T> empty(int page, int size) {
+        return of(Collections.emptyList(), 0, page, size);
+    }
+
+    public List<T> getItems() { return items; }
+    public long getTotal() { return total; }
+    public int getPage() { return page; }
+    public int getSize() { return size; }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/Result.java b/server/src/main/java/com/rocketmq/studio/common/domain/Result.java
new file mode 100644
index 0000000..b234e08
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/Result.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.common.domain;
+
+public class Result<T> {
+    private int code;
+    private String message;
+    private T data;
+
+    private Result() {}
+
+    private Result(int code, String message, T data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public static <T> Result<T> ok(T data) {
+        return new Result<>(200, "success", data);
+    }
+
+    public static <T> Result<T> ok() {
+        return new Result<>(200, "success", null);
+    }
+
+    public static <T> Result<T> error(int code, String message) {
+        return new Result<>(code, message, null);
+    }
+
+    public int getCode() { return code; }
+    public String getMessage() { return message; }
+    public T getData() { return data; }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/AlertLevel.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/AlertLevel.java
new file mode 100644
index 0000000..e1dc8f8
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/AlertLevel.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum AlertLevel {
+    error, warning, info
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/BrokerStatus.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/BrokerStatus.java
new file mode 100644
index 0000000..4396f7d
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/BrokerStatus.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum BrokerStatus {
+    running, readonly, maintenance
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/CertStatus.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/CertStatus.java
new file mode 100644
index 0000000..af1d284
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/CertStatus.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum CertStatus {
+    valid, expiring, expired
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/CertType.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/CertType.java
new file mode 100644
index 0000000..8b45b36
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/CertType.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum CertType {
+    TLS, mTLS, ServiceAccount
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClientLanguage.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClientLanguage.java
new file mode 100644
index 0000000..d87daeb
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClientLanguage.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum ClientLanguage {
+    Java, Go, Python, Rust, Cpp, CSharp, NodeJS, PHP
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClientType.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClientType.java
new file mode 100644
index 0000000..f244d18
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClientType.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum ClientType {
+    Producer, Consumer
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClusterStatus.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClusterStatus.java
new file mode 100644
index 0000000..5dd4307
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClusterStatus.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum ClusterStatus {
+    healthy, warning, error, offline
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClusterType.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClusterType.java
new file mode 100644
index 0000000..a10f3e2
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ClusterType.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum ClusterType {
+    V4_DIRECT, V5_PROXY_LOCAL, V5_PROXY_CLUSTER
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/ConsumeType.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ConsumeType.java
new file mode 100644
index 0000000..15e127c
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/ConsumeType.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum ConsumeType {
+    CLUSTERING, BROADCASTING
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/DeliveryStatus.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/DeliveryStatus.java
new file mode 100644
index 0000000..7c474f1
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/DeliveryStatus.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum DeliveryStatus {
+    success, failed, pending
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/FlushDiskType.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/FlushDiskType.java
new file mode 100644
index 0000000..6bba212
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/FlushDiskType.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum FlushDiskType {
+    ASYNC_FLUSH, SYNC_FLUSH
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/InstanceType.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/InstanceType.java
new file mode 100644
index 0000000..132b726
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/InstanceType.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum InstanceType {
+    PROXY, DIRECT
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/Protocol.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/Protocol.java
new file mode 100644
index 0000000..64a568a
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/Protocol.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum Protocol {
+    gRPC, Remoting
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/SubscriptionMode.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/SubscriptionMode.java
new file mode 100644
index 0000000..3a65255
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/SubscriptionMode.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum SubscriptionMode {
+    Push, Pop
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/TopicPerm.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/TopicPerm.java
new file mode 100644
index 0000000..cd3660a
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/TopicPerm.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum TopicPerm {
+    RW, RO, WO
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/domain/enums/TopicType.java b/server/src/main/java/com/rocketmq/studio/common/domain/enums/TopicType.java
new file mode 100644
index 0000000..1b2571b
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/domain/enums/TopicType.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.rocketmq.studio.common.domain.enums;
+
+public enum TopicType {
+    NORMAL, FIFO, DELAY, TRANSACTION, LITE
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/com/rocketmq/studio/common/exception/BusinessException.java
new file mode 100644
index 0000000..cb6c566
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/exception/BusinessException.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.rocketmq.studio.common.exception;
+
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+    private final int code;
+
+    public BusinessException(int code, String message) {
+        super(message);
+        this.code = code;
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/common/exception/GlobalExceptionHandler.java b/server/src/main/java/com/rocketmq/studio/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..3bc1c19
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/common/exception/GlobalExceptionHandler.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.common.exception;
+
+import com.rocketmq.studio.common.domain.Result;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+    @ExceptionHandler(BusinessException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public Result<?> handleBusinessException(BusinessException ex) {
+        log.warn("Business exception: {}", ex.getMessage());
+        return Result.error(ex.getCode(), ex.getMessage());
+    }
+
+    @ExceptionHandler(Exception.class)
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    public Result<?> handleException(Exception ex) {
+        log.error("Unexpected exception", ex);
+        return Result.error(500, "Internal Server Error");
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/auth/AuthControllerTest.java b/server/src/test/java/com/rocketmq/studio/auth/AuthControllerTest.java
new file mode 100644
index 0000000..b4fe26f
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/auth/AuthControllerTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.rocketmq.studio.auth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(AuthController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class AuthControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private AuthService authService;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Test
+    void loginShouldReturnTokenOnValidRequest() throws Exception {
+        LoginVO mockResponse = LoginVO.builder()
+                .token("mock-jwt-abc123")
+                .expiresIn(86400)
+                .user(LoginVO.UserInfo.builder()
+                        .username("testuser")
+                        .admin(false)
+                        .build())
+                .build();
+
+        when(authService.login(any(LoginDTO.class))).thenReturn(mockResponse);
+
+        LoginDTO request = new LoginDTO();
+        request.setUsername("testuser");
+        request.setPassword("testpass");
+
+        mockMvc.perform(post("/api/auth/login")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(request)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"))
+                .andExpect(jsonPath("$.data.token").value("mock-jwt-abc123"))
+                .andExpect(jsonPath("$.data.expiresIn").value(86400))
+                .andExpect(jsonPath("$.data.user.username").value("testuser"))
+                .andExpect(jsonPath("$.data.user.admin").value(false));
+    }
+
+    @Test
+    void loginShouldReturnAdminUserWhenAdminLogsIn() throws Exception {
+        LoginVO mockResponse = LoginVO.builder()
+                .token("mock-jwt-admin")
+                .expiresIn(86400)
+                .user(LoginVO.UserInfo.builder()
+                        .username("admin")
+                        .admin(true)
+                        .build())
+                .build();
+
+        when(authService.login(any(LoginDTO.class))).thenReturn(mockResponse);
+
+        LoginDTO request = new LoginDTO();
+        request.setUsername("admin");
+        request.setPassword("adminpass");
+
+        mockMvc.perform(post("/api/auth/login")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(request)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.user.username").value("admin"))
+                .andExpect(jsonPath("$.data.user.admin").value(true));
+    }
+
+    @Test
+    void logoutShouldReturnSuccess() throws Exception {
+        doNothing().when(authService).logout();
+
+        mockMvc.perform(post("/api/auth/logout"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.message").value("success"));
+
+        verify(authService).logout();
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/auth/AuthServiceTest.java b/server/src/test/java/com/rocketmq/studio/auth/AuthServiceTest.java
new file mode 100644
index 0000000..1452572
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/auth/AuthServiceTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.rocketmq.studio.auth;
+
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class AuthServiceTest {
+
+    private AuthService authService;
+
+    @BeforeEach
+    void setUp() {
+        authService = new AuthService();
+    }
+
+    @Test
+    void loginShouldReturnTokenForValidCredentials() {
+        LoginDTO request = new LoginDTO();
+        request.setUsername("testuser");
+        request.setPassword("testpass");
+
+        LoginVO response = authService.login(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getToken()).startsWith("mock-jwt-");
+        assertThat(response.getExpiresIn()).isEqualTo(86400);
+        assertThat(response.getUser()).isNotNull();
+        assertThat(response.getUser().getUsername()).isEqualTo("testuser");
+        assertThat(response.getUser().isAdmin()).isFalse();
+    }
+
+    @Test
+    void loginShouldReturnAdminFlagForAdminUser() {
+        LoginDTO request = new LoginDTO();
+        request.setUsername("admin");
+        request.setPassword("adminpass");
+
+        LoginVO response = authService.login(request);
+
+        assertThat(response.getUser().getUsername()).isEqualTo("admin");
+        assertThat(response.getUser().isAdmin()).isTrue();
+    }
+
+    @Test
+    void loginShouldThrowWhenUsernameIsNull() {
+        LoginDTO request = new LoginDTO();
+        request.setUsername(null);
+        request.setPassword("password");
+
+        assertThatThrownBy(() -> authService.login(request))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Username is required");
+    }
+
+    @Test
+    void loginShouldThrowWhenUsernameIsBlank() {
+        LoginDTO request = new LoginDTO();
+        request.setUsername("   ");
+        request.setPassword("password");
+
+        assertThatThrownBy(() -> authService.login(request))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Username is required");
+    }
+
+    @Test
+    void loginShouldThrowWhenPasswordIsNull() {
+        LoginDTO request = new LoginDTO();
+        request.setUsername("testuser");
+        request.setPassword(null);
+
+        assertThatThrownBy(() -> authService.login(request))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Password is required");
+    }
+
+    @Test
+    void loginShouldThrowWhenPasswordIsBlank() {
+        LoginDTO request = new LoginDTO();
+        request.setUsername("testuser");
+        request.setPassword("   ");
+
+        assertThatThrownBy(() -> authService.login(request))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Password is required");
+    }
+
+    @Test
+    void logoutShouldCompleteWithoutError() {
+        authService.logout();
+    }
+}