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 <[email protected]>
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");
+    }
+}

Reply via email to