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

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

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 6670064d2e84a3a0c8a44688f21ef807f3d44a54
Author: lizhimins <70...@qq.com>
AuthorDate: Fri Jul 3 16:52:00 2026 +0800

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

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