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


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new f3464698 fix(ui): batch of frontend concurrency and correctness fixes 
(#1352, #1354, #1355, #1356, #1357, #1358, #1359, #1360, #1361, #1362, #1363, 
#1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372)
f3464698 is described below

commit f34646987bb79b4451013c52a8ee453f0002ec91
Author: lizhimins <[email protected]>
AuthorDate: Mon Aug 10 12:20:37 2026 +0800

    fix(ui): batch of frontend concurrency and correctness fixes (#1352, #1354, 
#1355, #1356, #1357, #1358, #1359, #1360, #1361, #1362, #1363, #1364, #1365, 
#1366, #1367, #1368, #1369, #1370, #1371, #1372)
    
    * fix(ui): remove duplicate cluster refresh timer
    
    * fix(ui): ignore stale cluster list responses
    
    * fix(ui): format IPv6 proxy endpoints
    
    * fix(ui): preserve unknown broker status
    
    * fix(ui): ignore stale Proxy list responses
    
    * fix(ui): await Proxy mutation refreshes
    
    * fix(consumer): scope global detail diagnostics
    
    * fix(consumer): show offline group details
    
    * fix(consumer): coalesce group refreshes
    
    * fix(consumer): label manual reload as refresh
    
    * fix(consumer): use instance-scoped row keys
    
    * fix(ops): serialize channel setting updates
    
    * fix(grafana): isolate dashboard preview requests
    
    * fix(alerts): isolate asset preview requests
    
    * fix(assets): track concurrent exports independently
    
    * fix(acl): load rules and users independently
    
    * fix(acl): preserve missing creation timestamps
    
    * fix(alerts): handle unknown system alert levels
    
    * fix(alerts): track concurrent acknowledgements
    
    * fix(certs): track concurrent renewals
    
    ---------
    
    Co-authored-by: Yue Wang <[email protected]>
    Co-authored-by: yyqdbngt <[email protected]>
---
 web/src/api/acl.ts                                 |   4 +-
 web/src/components/AlertRuleAssetList.tsx          |  41 ++++++--
 web/src/components/GrafanaDashboardList.tsx        |  41 ++++++--
 .../__tests__/AlertRuleAssetList.test.tsx          |  50 +++++++++-
 .../__tests__/GrafanaDashboardList.test.tsx        |  61 +++++++++++-
 .../pages/cluster/__tests__/K8sCertsPage.test.tsx  |  30 +++++-
 web/src/pages/cluster/certs.tsx                    |  12 ++-
 web/src/pages/instance/__tests__/AclPage.test.tsx  |  56 +++++++++++
 web/src/pages/instance/acl.tsx                     |  38 +++++---
 .../pages/ops/__tests__/SystemAlertsPage.test.tsx  | 106 +++++++++++++++++++++
 web/src/pages/ops/systemAlerts.tsx                 |  27 ++++--
 web/src/pages/studio/BrokerCluster.tsx             |  41 +++++---
 web/src/pages/studio/GroupManagement.tsx           |  68 +++++++++----
 web/src/pages/studio/Ops.tsx                       |  28 +++++-
 web/src/pages/studio/Proxy.tsx                     |  77 ++++++++-------
 .../pages/studio/__tests__/BrokerCluster.test.tsx  |  75 ++++++++++++++-
 .../studio/__tests__/GroupManagement.test.tsx      |  82 ++++++++++++----
 web/src/pages/studio/__tests__/Ops.test.tsx        |  24 ++++-
 web/src/pages/studio/__tests__/Proxy.test.tsx      |  35 ++++++-
 19 files changed, 744 insertions(+), 152 deletions(-)

diff --git a/web/src/api/acl.ts b/web/src/api/acl.ts
index 63b710dd..41fdb22b 100644
--- a/web/src/api/acl.ts
+++ b/web/src/api/acl.ts
@@ -11,7 +11,7 @@ export interface AclRule {
   decision: string;
   scope: string;
   aclVersion: number | string;
-  createdAt: string;
+  createdAt?: string | null;
 }
 
 export interface AclRuleQuery {
@@ -26,7 +26,7 @@ export interface AclUser {
   secretKey: string;
   admin: boolean;
   clusters: string[];
-  createdAt: string;
+  createdAt?: string | null;
 }
 
 export async function listAclRules(params?: AclRuleQuery) {
diff --git a/web/src/components/AlertRuleAssetList.tsx 
b/web/src/components/AlertRuleAssetList.tsx
index 32b2a232..2614d001 100644
--- a/web/src/components/AlertRuleAssetList.tsx
+++ b/web/src/components/AlertRuleAssetList.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { App, Button, Modal, Space, Table, Tag, Typography } from 'antd';
 import type { ColumnsType } from 'antd/es/table';
 import { DownloadSimple, Eye } from '@phosphor-icons/react';
@@ -43,7 +43,8 @@ export const AlertRuleAssetList: React.FC = () => {
   const [viewing, setViewing] = useState<AlertRuleAssetInfo | null>(null);
   const [viewContent, setViewContent] = useState('');
   const [viewLoading, setViewLoading] = useState(false);
-  const [exportingName, setExportingName] = useState<string | null>(null);
+  const viewRequestId = useRef(0);
+  const [exportingNames, setExportingNames] = useState<Set<string>>(() => new 
Set());
 
   useEffect(() => {
     let cancelled = false;
@@ -60,22 +61,38 @@ export const AlertRuleAssetList: React.FC = () => {
     void load();
     return () => {
       cancelled = true;
+      viewRequestId.current += 1;
     };
   }, [t, message]);
 
   const handleView = async (info: AlertRuleAssetInfo) => {
+    const requestId = ++viewRequestId.current;
     setViewing(info);
+    setViewContent('');
     setViewLoading(true);
     try {
       const yaml = await getAlertRuleAsset(info.name);
-      setViewContent(yaml);
+      if (requestId === viewRequestId.current) {
+        setViewContent(yaml);
+      }
     } catch {
-      message.error(t('alertAssets.loadFailed'));
+      if (requestId === viewRequestId.current) {
+        message.error(t('alertAssets.loadFailed'));
+      }
     } finally {
-      setViewLoading(false);
+      if (requestId === viewRequestId.current) {
+        setViewLoading(false);
+      }
     }
   };
 
+  const closeView = () => {
+    viewRequestId.current += 1;
+    setViewing(null);
+    setViewContent('');
+    setViewLoading(false);
+  };
+
   const triggerDownload = (name: string, content: Blob | string) => {
     const blob = typeof content === 'string' ? new Blob([content], { type: 
'text/yaml' }) : content;
     const url = URL.createObjectURL(blob);
@@ -87,7 +104,7 @@ export const AlertRuleAssetList: React.FC = () => {
   };
 
   const handleExport = async (info: AlertRuleAssetInfo) => {
-    setExportingName(info.name);
+    setExportingNames((current) => new Set(current).add(info.name));
     try {
       const blob = await exportAlertRuleAsset(info.name);
       triggerDownload(info.name, blob);
@@ -95,7 +112,11 @@ export const AlertRuleAssetList: React.FC = () => {
     } catch {
       message.error(t('alertAssets.exportFailed'));
     } finally {
-      setExportingName(null);
+      setExportingNames((current) => {
+        const next = new Set(current);
+        next.delete(info.name);
+        return next;
+      });
     }
   };
 
@@ -144,7 +165,7 @@ export const AlertRuleAssetList: React.FC = () => {
           <Button
             size="small"
             icon={<DownloadSimple size={16} />}
-            loading={exportingName === record.name}
+            loading={exportingNames.has(record.name)}
             onClick={() => handleExport(record)}
           >
             {t('common.export')}
@@ -168,8 +189,8 @@ export const AlertRuleAssetList: React.FC = () => {
       <Modal
         title={viewing ? viewing.name : t('alertAssets.title')}
         open={viewing !== null}
-        footer={<Button onClick={() => 
setViewing(null)}>{t('common.close')}</Button>}
-        onCancel={() => setViewing(null)}
+        footer={<Button onClick={closeView}>{t('common.close')}</Button>}
+        onCancel={closeView}
         width={760}
         destroyOnHidden
       >
diff --git a/web/src/components/GrafanaDashboardList.tsx 
b/web/src/components/GrafanaDashboardList.tsx
index 894fb22d..251ae6d3 100644
--- a/web/src/components/GrafanaDashboardList.tsx
+++ b/web/src/components/GrafanaDashboardList.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { App, Button, Modal, Space, Table, Tag, Typography } from 'antd';
 import type { ColumnsType } from 'antd/es/table';
 import { DownloadSimple, Eye } from '@phosphor-icons/react';
@@ -37,7 +37,8 @@ export const GrafanaDashboardList: React.FC = () => {
   const [viewing, setViewing] = useState<GrafanaDashboardInfo | null>(null);
   const [viewContent, setViewContent] = useState('');
   const [viewLoading, setViewLoading] = useState(false);
-  const [exportingUid, setExportingUid] = useState<string | null>(null);
+  const viewRequestId = useRef(0);
+  const [exportingUids, setExportingUids] = useState<Set<string>>(() => new 
Set());
 
   useEffect(() => {
     let cancelled = false;
@@ -54,22 +55,38 @@ export const GrafanaDashboardList: React.FC = () => {
     void load();
     return () => {
       cancelled = true;
+      viewRequestId.current += 1;
     };
   }, [t, message]);
 
   const handleView = async (info: GrafanaDashboardInfo) => {
+    const requestId = ++viewRequestId.current;
     setViewing(info);
+    setViewContent('');
     setViewLoading(true);
     try {
       const model = await getGrafanaDashboard(info.uid);
-      setViewContent(JSON.stringify(model, null, 2));
+      if (requestId === viewRequestId.current) {
+        setViewContent(JSON.stringify(model, null, 2));
+      }
     } catch {
-      message.error(t('grafana.loadFailed'));
+      if (requestId === viewRequestId.current) {
+        message.error(t('grafana.loadFailed'));
+      }
     } finally {
-      setViewLoading(false);
+      if (requestId === viewRequestId.current) {
+        setViewLoading(false);
+      }
     }
   };
 
+  const closeView = () => {
+    viewRequestId.current += 1;
+    setViewing(null);
+    setViewContent('');
+    setViewLoading(false);
+  };
+
   const triggerDownload = (uid: string, content: Blob | string) => {
     const blob =
       typeof content === 'string' ? new Blob([content], { type: 
'application/json' }) : content;
@@ -82,7 +99,7 @@ export const GrafanaDashboardList: React.FC = () => {
   };
 
   const handleExport = async (info: GrafanaDashboardInfo) => {
-    setExportingUid(info.uid);
+    setExportingUids((current) => new Set(current).add(info.uid));
     try {
       const blob = await exportGrafanaDashboard(info.uid);
       triggerDownload(info.uid, blob);
@@ -90,7 +107,11 @@ export const GrafanaDashboardList: React.FC = () => {
     } catch {
       message.error(t('grafana.exportFailed'));
     } finally {
-      setExportingUid(null);
+      setExportingUids((current) => {
+        const next = new Set(current);
+        next.delete(info.uid);
+        return next;
+      });
     }
   };
 
@@ -133,7 +154,7 @@ export const GrafanaDashboardList: React.FC = () => {
           <Button
             size="small"
             icon={<DownloadSimple size={16} />}
-            loading={exportingUid === record.uid}
+            loading={exportingUids.has(record.uid)}
             onClick={() => handleExport(record)}
           >
             {t('common.export')}
@@ -157,8 +178,8 @@ export const GrafanaDashboardList: React.FC = () => {
       <Modal
         title={viewing ? viewing.title : t('grafana.title')}
         open={viewing !== null}
-        footer={<Button onClick={() => 
setViewing(null)}>{t('common.close')}</Button>}
-        onCancel={() => setViewing(null)}
+        footer={<Button onClick={closeView}>{t('common.close')}</Button>}
+        onCancel={closeView}
         width={760}
         destroyOnHidden
       >
diff --git a/web/src/components/__tests__/AlertRuleAssetList.test.tsx 
b/web/src/components/__tests__/AlertRuleAssetList.test.tsx
index cf606cdc..5da6d383 100644
--- a/web/src/components/__tests__/AlertRuleAssetList.test.tsx
+++ b/web/src/components/__tests__/AlertRuleAssetList.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
-import { fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
+import { act, fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
 import { App as AntdApp } from 'antd';
 import AlertRuleAssetList from '../AlertRuleAssetList';
 import { LangProvider } from '../../i18n/LangContext';
@@ -97,6 +97,54 @@ describe('AlertRuleAssetList', () => {
     
expect(alertRuleAssetService.getAlertRuleAsset).toHaveBeenCalledWith('rocketmq-broker-down');
   });
 
+  it('keeps the latest preview when an earlier request resolves last', async 
() => {
+    
vi.mocked(alertRuleAssetService.listAlertRuleAssets).mockResolvedValue(sampleAssets);
+    let resolveBroker!: (value: string) => void;
+    let resolveConsumer!: (value: string) => void;
+    vi.mocked(alertRuleAssetService.getAlertRuleAsset).mockImplementation(
+      (name) =>
+        new Promise((resolve) => {
+          if (name === 'rocketmq-broker-down') resolveBroker = resolve;
+          else resolveConsumer = resolve;
+        }),
+    );
+    renderWithProviders(<AlertRuleAssetList />);
+
+    const viewButtons = await screen.findAllByRole('button', { name: /查看|View/ 
});
+    fireEvent.click(viewButtons[0]);
+    fireEvent.click(viewButtons[1]);
+
+    await act(async () => {
+      resolveConsumer('alert: LATEST_CONSUMER_ALERT');
+    });
+    const dialog = await screen.findByRole('dialog');
+    
expect(within(dialog).getByText(/LATEST_CONSUMER_ALERT/)).toBeInTheDocument();
+
+    await act(async () => {
+      resolveBroker('alert: STALE_BROKER_ALERT');
+    });
+    
expect(within(dialog).getByText(/LATEST_CONSUMER_ALERT/)).toBeInTheDocument();
+    
expect(within(dialog).queryByText(/STALE_BROKER_ALERT/)).not.toBeInTheDocument();
+  });
+
+  it('tracks simultaneous asset exports independently', async () => {
+    
vi.mocked(alertRuleAssetService.listAlertRuleAssets).mockResolvedValue(sampleAssets);
+    vi.mocked(alertRuleAssetService.exportAlertRuleAsset).mockImplementation(
+      () => new Promise(() => {}),
+    );
+    renderWithProviders(<AlertRuleAssetList />);
+
+    const exportButtons = await screen.findAllByRole('button', { name: 
/导出|Export/ });
+    fireEvent.click(exportButtons[0]);
+    fireEvent.click(exportButtons[1]);
+
+    await waitFor(() => {
+      
expect(alertRuleAssetService.exportAlertRuleAsset).toHaveBeenCalledTimes(2);
+      expect(exportButtons[0]).toHaveClass('ant-btn-loading');
+      expect(exportButtons[1]).toHaveClass('ant-btn-loading');
+    });
+  });
+
   it('downloads the yaml when Export is clicked', async () => {
     const createObjectURLSpy = vi.spyOn(URL, 
'createObjectURL').mockReturnValue('blob:url');
     const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() 
=> {});
diff --git a/web/src/components/__tests__/GrafanaDashboardList.test.tsx 
b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
index cc2c24f9..618ecb4c 100644
--- a/web/src/components/__tests__/GrafanaDashboardList.test.tsx
+++ b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { App } from 'antd';
-import { render, screen, waitFor, within } from '@testing-library/react';
+import { act, render, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 
'vitest';
 
@@ -112,6 +112,65 @@ describe('GrafanaDashboardList', () => {
     expect(within(dialog).getByText(/"uid": 
"rocketmq-overview"/)).toBeInTheDocument();
   });
 
+  it('keeps the latest preview when an earlier request resolves last', async 
() => {
+    let resolveOverview!: (value: typeof dashboardModel) => void;
+    let resolveBroker!: (value: typeof dashboardModel) => void;
+    vi.mocked(getGrafanaDashboard).mockImplementation(
+      (uid) =>
+        new Promise((resolve) => {
+          if (uid === 'rocketmq-overview') resolveOverview = resolve;
+          else resolveBroker = resolve;
+        }),
+    );
+    render(
+      <App>
+        <LangProvider>
+          <GrafanaDashboardList />
+        </LangProvider>
+      </App>,
+    );
+
+    await screen.findByText('RocketMQ Cluster Overview');
+    const viewButtons = screen.getAllByRole('button', { name: /View|查看/ });
+    await userEvent.click(viewButtons[0]);
+    await userEvent.click(viewButtons[1]);
+
+    await act(async () => {
+      resolveBroker({ ...dashboardModel, uid: 'rocketmq-broker', title: 
'RocketMQ Broker' });
+    });
+    const dialog = await screen.findByRole('dialog');
+    expect(within(dialog).getByText(/"uid": 
"rocketmq-broker"/)).toBeInTheDocument();
+
+    await act(async () => {
+      resolveOverview(dashboardModel);
+    });
+    expect(within(dialog).getByText(/"uid": 
"rocketmq-broker"/)).toBeInTheDocument();
+    expect(within(dialog).queryByText(/"uid": 
"rocketmq-overview"/)).not.toBeInTheDocument();
+  });
+
+  it('tracks simultaneous dashboard exports independently', async () => {
+    vi.mocked(exportGrafanaDashboard).mockImplementation(() => new Promise(() 
=> {}));
+    const user = userEvent.setup();
+    render(
+      <App>
+        <LangProvider>
+          <GrafanaDashboardList />
+        </LangProvider>
+      </App>,
+    );
+
+    await screen.findByText('RocketMQ Cluster Overview');
+    const exportButtons = screen.getAllByRole('button', { name: /Export|导出/ });
+    await user.click(exportButtons[0]);
+    await user.click(exportButtons[1]);
+
+    await waitFor(() => {
+      expect(exportGrafanaDashboard).toHaveBeenCalledTimes(2);
+      expect(exportButtons[0]).toHaveClass('ant-btn-loading');
+      expect(exportButtons[1]).toHaveClass('ant-btn-loading');
+    });
+  });
+
   it('exports a dashboard and triggers a download', async () => {
     const user = userEvent.setup();
     const createObjectURL = vi.fn().mockReturnValue('blob:grafana');
diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx 
b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
index bd6eccb2..6b4903b5 100644
--- a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
@@ -16,11 +16,11 @@
  */
 
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { fireEvent, render, screen } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { App } from 'antd';
+import { App, Modal } from 'antd';
 import type { K8sCertInfo } from '../../../api/cluster';
-import { listK8sCerts } from '../../../services/clusterService';
+import { listK8sCerts, renewK8sCert } from '../../../services/clusterService';
 import K8sCertsPage from '../certs';
 
 vi.mock('../../../services/clusterService', () => ({
@@ -79,6 +79,7 @@ beforeAll(() => {
 
 describe('K8sCertsPage', () => {
   beforeEach(() => {
+    vi.clearAllMocks();
     vi.mocked(listK8sCerts).mockResolvedValue(certs);
   });
 
@@ -137,6 +138,29 @@ describe('K8sCertsPage', () => {
     expect(screen.queryByText('rocketmq-prod-tls')).not.toBeInTheDocument();
   });
 
+  it('tracks simultaneous certificate renewals independently', async () => {
+    vi.mocked(renewK8sCert).mockImplementation(() => new Promise(() => {}));
+    const confirmSpy = vi.spyOn(Modal, 'confirm');
+    renderPage();
+
+    await screen.findByText('rocketmq-prod-tls');
+    const renewButtons = screen.getAllByRole('button', { name: /续期/ });
+    fireEvent.click(renewButtons[0]);
+    fireEvent.click(renewButtons[1]);
+    expect(confirmSpy).toHaveBeenCalledTimes(2);
+
+    void confirmSpy.mock.calls[0][0].onOk?.(() => {});
+    void confirmSpy.mock.calls[1][0].onOk?.(() => {});
+
+    await waitFor(() => {
+      expect(renewK8sCert).toHaveBeenCalledWith('cert-prod');
+      expect(renewK8sCert).toHaveBeenCalledWith('cert-staging');
+      expect(renewButtons[0]).toHaveClass('ant-btn-loading');
+      expect(renewButtons[1]).toHaveClass('ant-btn-loading');
+    });
+    confirmSpy.mockRestore();
+  });
+
   it('trims certificate search text before filtering', async () => {
     const user = userEvent.setup();
     renderPage();
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index e0b95bdc..400db782 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -57,7 +57,7 @@ const K8sCertsPage = () => {
   const [certs, setCerts] = useState<K8sCertInfo[]>([]);
   const [loading, setLoading] = useState(true);
   const [submitting, setSubmitting] = useState(false);
-  const [renewingId, setRenewingId] = useState<string | null>(null);
+  const [renewingIds, setRenewingIds] = useState<Set<string>>(() => new Set());
   const [certSearch, setCertSearch] = useState('');
   const [certTypeFilter, setCertTypeFilter] = useState<string>('');
   const [certNamespaceFilter, setCertNamespaceFilter] = useState<string>('');
@@ -146,7 +146,7 @@ const K8sCertsPage = () => {
   });
 
   const renewCert = async (cert: K8sCertInfo) => {
-    setRenewingId(cert.id);
+    setRenewingIds((current) => new Set(current).add(cert.id));
     try {
       const renewed = await renewK8sCert(cert.id);
       setCerts((prev) => prev.map((item) => (item.id === renewed.id ? renewed 
: item)));
@@ -155,7 +155,11 @@ const K8sCertsPage = () => {
       message.error(getErrorMessage(error));
       throw error;
     } finally {
-      setRenewingId(null);
+      setRenewingIds((current) => {
+        const next = new Set(current);
+        next.delete(cert.id);
+        return next;
+      });
     }
   };
 
@@ -297,7 +301,7 @@ const K8sCertsPage = () => {
           <Button
             size="small"
             icon={<SyncOutlined />}
-            loading={renewingId === record.id}
+            loading={renewingIds.has(record.id)}
             onClick={() => {
               Modal.confirm({
                 title: '确认续期',
diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx 
b/web/src/pages/instance/__tests__/AclPage.test.tsx
index bd4b4104..a67a9725 100644
--- a/web/src/pages/instance/__tests__/AclPage.test.tsx
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -103,6 +103,24 @@ describe('ACL page', () => {
     expect(aclService.listAclUsers).toHaveBeenCalledTimes(1);
   });
 
+  it('keeps rules available when loading users fails', async () => {
+    vi.mocked(aclService.listAclUsers).mockRejectedValue(new Error('users 
unavailable'));
+    renderWithProviders(<AclPage />);
+
+    expect(await screen.findByText('remote-user')).toBeInTheDocument();
+    expect(screen.getByText('remote-topic')).toBeInTheDocument();
+  });
+
+  it('keeps users available when loading rules fails', async () => {
+    const user = userEvent.setup();
+    vi.mocked(aclService.listAclRules).mockRejectedValue(new Error('rules 
unavailable'));
+    renderWithProviders(<AclPage />);
+
+    await user.click(await screen.findByText('用户管理'));
+    expect(await screen.findByText('remote-admin')).toBeInTheDocument();
+    expect(screen.getByText('cluster-a')).toBeInTheDocument();
+  });
+
   it('renders backend users on the user tab', async () => {
     const user = userEvent.setup();
     renderWithProviders(<AclPage />);
@@ -113,6 +131,44 @@ describe('ACL page', () => {
     expect(screen.getByText('cluster-a')).toBeInTheDocument();
   });
 
+  it('shows missing backend timestamps as unavailable', async () => {
+    const user = userEvent.setup();
+    vi.mocked(aclService.listAclRules).mockResolvedValue([
+      {
+        id: 'rule-without-time',
+        principal: 'no-time-rule',
+        resource: 'topic-a',
+        resourceType: 'Topic',
+        resourcePattern: 'LITERAL',
+        actions: ['PUB'],
+        decision: 'ALLOW',
+        scope: 'cluster',
+        aclVersion: 2,
+        createdAt: null,
+      },
+    ]);
+    vi.mocked(aclService.listAclUsers).mockResolvedValue([
+      {
+        id: 'user-without-time',
+        username: 'no-time-user',
+        accessKey: 'acce****3456',
+        secretKey: 'secr****7654',
+        admin: false,
+        clusters: [],
+        createdAt: null,
+      },
+    ]);
+    renderWithProviders(<AclPage />);
+
+    expect(await screen.findByText('no-time-rule')).toBeInTheDocument();
+    expect(screen.getByText('-')).toBeInTheDocument();
+
+    await user.click(screen.getByText('用户管理'));
+    const userPanel = screen.getByRole('tabpanel', { name: '用户管理' });
+    expect(await 
within(userPanel).findByText('no-time-user')).toBeInTheDocument();
+    expect(within(userPanel).getByText('-')).toBeInTheDocument();
+  });
+
   it('does not submit masked credentials when editing a user', async () => {
     const user = userEvent.setup();
     vi.mocked(aclService.updateAclUser).mockResolvedValue({
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index 85ae8a80..e6e3e469 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -70,7 +70,7 @@ const normalizeRule = (rule: AclRule): AclRule => ({
   decision: rule.decision ?? '',
   scope: rule.scope ?? '',
   aclVersion: rule.aclVersion ?? '2.0',
-  createdAt: rule.createdAt ?? new Date().toISOString(),
+  createdAt: rule.createdAt ?? null,
 });
 
 const normalizeUser = (user: AclUser): AclUser => ({
@@ -80,7 +80,7 @@ const normalizeUser = (user: AclUser): AclUser => ({
   secretKey: user.secretKey ?? '',
   admin: user.admin ?? false,
   clusters: user.clusters ?? [],
-  createdAt: user.createdAt ?? new Date().toISOString(),
+  createdAt: user.createdAt ?? null,
 });
 
 const isFormValidationError = (error: unknown) =>
@@ -126,19 +126,26 @@ const AclPage = () => {
   useEffect(() => {
     let mounted = true;
 
-    Promise.all([listAclRules(), listAclUsers()])
-      .then(([nextRules, nextUsers]) => {
-        if (!mounted) return;
-        setRules(nextRules.map(normalizeRule));
-        setUsers(nextUsers.map(normalizeUser));
+    void listAclRules()
+      .then((nextRules) => {
+        if (mounted) setRules(nextRules.map(normalizeRule));
       })
       .catch(() => {
         if (mounted) message.error(t('common.fetchDataFailed'));
       })
       .finally(() => {
-        if (!mounted) return;
-        setRulesLoading(false);
-        setUsersLoading(false);
+        if (mounted) setRulesLoading(false);
+      });
+
+    void listAclUsers()
+      .then((nextUsers) => {
+        if (mounted) setUsers(nextUsers.map(normalizeUser));
+      })
+      .catch(() => {
+        if (mounted) message.error(t('common.fetchDataFailed'));
+      })
+      .finally(() => {
+        if (mounted) setUsersLoading(false);
       });
 
     return () => {
@@ -343,7 +350,8 @@ const AclPage = () => {
     }
   };
 
-  const formatDate = (iso: string) => {
+  const formatDate = (iso?: string | null) => {
+    if (!iso) return '-';
     const d = new Date(iso);
     if (Number.isNaN(d.getTime())) return '-';
     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')}`;
@@ -443,8 +451,8 @@ const AclPage = () => {
       dataIndex: 'createdAt',
       key: 'createdAt',
       width: 160,
-      sorter: (a, b) => a.createdAt.localeCompare(b.createdAt),
-      render: (iso: string) => (
+      sorter: (a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? ''),
+      render: (iso?: string | null) => (
         <span style={{ fontSize: 13, color: '#8c8c8c' 
}}>{formatDate(iso)}</span>
       ),
     },
@@ -588,8 +596,8 @@ const AclPage = () => {
       dataIndex: 'createdAt',
       key: 'createdAt',
       width: 160,
-      sorter: (a, b) => a.createdAt.localeCompare(b.createdAt),
-      render: (iso: string) => (
+      sorter: (a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? ''),
+      render: (iso?: string | null) => (
         <span style={{ fontSize: 13, color: '#8c8c8c' 
}}>{formatDate(iso)}</span>
       ),
     },
diff --git a/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx 
b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
new file mode 100644
index 00000000..52f7d17e
--- /dev/null
+++ b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0.
+ */
+
+import { App } from 'antd';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { LangProvider } from '../../../i18n/LangContext';
+import { acknowledgeAlert, listSystemAlerts } from 
'../../../services/opsService';
+import SystemAlertsPage from '../systemAlerts';
+
+vi.mock('../../../services/opsService', () => ({
+  acknowledgeAlert: vi.fn(),
+  clearAcknowledgedAlerts: vi.fn(),
+  listSystemAlerts: vi.fn(),
+}));
+
+beforeAll(() => {
+  Object.defineProperty(window, 'matchMedia', {
+    writable: true,
+    value: vi.fn().mockImplementation((query: string) => ({
+      matches: false,
+      media: query,
+      onchange: null,
+      addListener: vi.fn(),
+      removeListener: vi.fn(),
+      addEventListener: vi.fn(),
+      removeEventListener: vi.fn(),
+      dispatchEvent: vi.fn(),
+    })),
+  });
+});
+
+const renderPage = () =>
+  render(
+    <App>
+      <LangProvider>
+        <SystemAlertsPage />
+      </LangProvider>
+    </App>,
+  );
+
+describe('SystemAlertsPage', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.mocked(listSystemAlerts).mockResolvedValue([
+      {
+        id: 'alert-a',
+        level: 'error',
+        title: 'Broker unavailable',
+        description: 'broker a',
+        time: '2026-08-10 01:00',
+        acknowledged: false,
+      },
+      {
+        id: 'alert-b',
+        level: 'warning',
+        title: 'Consumer lag',
+        description: 'consumer b',
+        time: '2026-08-10 01:01',
+        acknowledged: false,
+      },
+    ]);
+  });
+
+  it('renders an alert with an unknown backend level', async () => {
+    vi.mocked(listSystemAlerts).mockResolvedValue([
+      {
+        id: 'alert-critical',
+        level: 'critical',
+        title: 'Critical broker condition',
+        description: 'A newer backend emitted this level',
+        time: '2026-08-10 01:00',
+        acknowledged: false,
+      },
+    ]);
+
+    renderPage();
+
+    expect(await screen.findByText('Critical broker 
condition')).toBeInTheDocument();
+    expect(screen.getByText('critical')).toBeInTheDocument();
+    expect(screen.getByText('A newer backend emitted this 
level')).toBeInTheDocument();
+  });
+
+  it('tracks simultaneous acknowledgements independently', async () => {
+    vi.mocked(acknowledgeAlert).mockImplementation(() => new Promise(() => 
{}));
+    const user = userEvent.setup();
+    renderPage();
+
+    await screen.findByText('Broker unavailable');
+    const acknowledgeButtons = screen.getAllByRole('button', { name: /^确认$/ });
+    await user.click(acknowledgeButtons[0]);
+    await user.click(acknowledgeButtons[1]);
+
+    await waitFor(() => {
+      expect(acknowledgeAlert).toHaveBeenCalledWith('alert-a');
+      expect(acknowledgeAlert).toHaveBeenCalledWith('alert-b');
+      expect(acknowledgeButtons[0]).toHaveClass('ant-btn-loading');
+      expect(acknowledgeButtons[1]).toHaveClass('ant-btn-loading');
+    });
+  });
+});
diff --git a/web/src/pages/ops/systemAlerts.tsx 
b/web/src/pages/ops/systemAlerts.tsx
index 09e59041..8f73da59 100644
--- a/web/src/pages/ops/systemAlerts.tsx
+++ b/web/src/pages/ops/systemAlerts.tsx
@@ -41,7 +41,7 @@ const SystemAlertsPage = () => {
   const [alerts, setAlerts] = useState<SystemAlert[]>([]);
   const [levelFilter, setLevelFilter] = useState<string>('all');
   const [loading, setLoading] = useState(true);
-  const [acknowledgingId, setAcknowledgingId] = useState<string | null>(null);
+  const [acknowledgingIds, setAcknowledgingIds] = useState<Set<string>>(() => 
new Set());
   const [clearing, setClearing] = useState(false);
 
   useEffect(() => {
@@ -68,7 +68,7 @@ const SystemAlertsPage = () => {
   const unackCount = alerts.filter((a) => !a.acknowledged).length;
 
   const handleAck = async (id: string) => {
-    setAcknowledgingId(id);
+    setAcknowledgingIds((current) => new Set(current).add(id));
     try {
       await acknowledgeAlert(id);
       setAlerts((prev) => prev.map((a) => (a.id === id ? { ...a, acknowledged: 
true } : a)));
@@ -76,7 +76,11 @@ const SystemAlertsPage = () => {
     } catch {
       message.error('确认告警失败,请稍后重试');
     } finally {
-      setAcknowledgingId(null);
+      setAcknowledgingIds((current) => {
+        const next = new Set(current);
+        next.delete(id);
+        return next;
+      });
     }
   };
 
@@ -138,7 +142,12 @@ const SystemAlertsPage = () => {
         {loading && <Card loading />}
         {!loading &&
           filtered.map((alert) => {
-            const cfg = alertLevelConfig[alert.level];
+            const normalizedLevel = alert.level.toLowerCase();
+            const cfg = alertLevelConfig[normalizedLevel] ?? {
+              color: '#8c8c8c',
+              bg: '#fafafa',
+              label: alert.level || t('common.na'),
+            };
             return (
               <div
                 key={alert.id}
@@ -160,11 +169,13 @@ const SystemAlertsPage = () => {
                     </Text>
                     <Tag
                       color={
-                        alert.level === 'error'
+                        normalizedLevel === 'error'
                           ? 'error'
-                          : alert.level === 'warning'
+                          : normalizedLevel === 'warning'
                             ? 'warning'
-                            : 'processing'
+                            : normalizedLevel === 'info'
+                              ? 'processing'
+                              : 'default'
                       }
                       style={{ fontSize: 11, lineHeight: '18px', padding: '0 
6px' }}
                     >
@@ -185,7 +196,7 @@ const SystemAlertsPage = () => {
                       type="link"
                       icon={<CheckCircle size={14} />}
                       onClick={() => handleAck(alert.id)}
-                      loading={acknowledgingId === alert.id}
+                      loading={acknowledgingIds.has(alert.id)}
                     >
                       {t('sysAlerts.acknowledge')}
                     </Button>
diff --git a/web/src/pages/studio/BrokerCluster.tsx 
b/web/src/pages/studio/BrokerCluster.tsx
index 702af537..e9f1b549 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 import {
   Table,
   Button,
@@ -42,7 +42,7 @@ import { listClusters, restartBroker } from 
'../../services/clusterService';
 import type { ClusterInfo } from '../../api/cluster';
 
 // ─── Types ──────────────────────────────────────────────────────
-type NodeStatus = 'running' | 'readonly' | 'maintenance';
+type NodeStatus = 'running' | 'readonly' | 'maintenance' | 'unknown';
 
 const REFRESH_INTERVAL_MS = 2000;
 
@@ -85,10 +85,20 @@ const normalizeStatus = (status: string): NodeStatus => {
   const value = (status || '').toLowerCase();
   if (value === 'readonly' || value === 'warning') return 'readonly';
   if (value === 'maintenance' || value === 'error' || value === 'offline') 
return 'maintenance';
-  return 'running';
+  if (value === 'running' || value === 'healthy') return 'running';
+  return 'unknown';
 };
 
-const hostOf = (addr: string): string => addr.split(':')[0] ?? addr;
+const hostOf = (addr: string): string => {
+  const value = addr.trim();
+  if (value.startsWith('[')) {
+    const closingBracket = value.indexOf(']');
+    return closingBracket >= 0 ? value.slice(0, closingBracket + 1) : value;
+  }
+  const firstColon = value.indexOf(':');
+  const lastColon = value.lastIndexOf(':');
+  return firstColon >= 0 && firstColon === lastColon ? value.slice(0, 
lastColon) : value;
+};
 
 function mapClusters(clusters: ClusterInfo[]): {
   brokers: BrokerRecord[];
@@ -155,33 +165,30 @@ const BrokerClusterPage = () => {
   const [brokerData, setBrokerData] = useState<BrokerRecord[]>([]);
   const [nameServerData, setNameServerData] = useState<NameServerRecord[]>([]);
   const [proxyData, setProxyData] = useState<ProxyRecord[]>([]);
+  const loadRequestId = useRef(0);
   const { t } = useLang();
   const { message } = App.useApp();
 
   const loadData = useCallback(async () => {
+    const requestId = ++loadRequestId.current;
     setLoading(true);
     try {
       const clusters = await listClusters();
+      if (requestId !== loadRequestId.current) return;
       const mapped = mapClusters(clusters);
       setBrokerData(mapped.brokers);
       setNameServerData(mapped.nameServers);
       setProxyData(mapped.proxies);
     } catch {
+      if (requestId !== loadRequestId.current) return;
       message.error(t('common.refreshFailed'));
     } finally {
-      setLoading(false);
+      if (requestId === loadRequestId.current) {
+        setLoading(false);
+      }
     }
   }, [message, t]);
 
-  // Live refresh: poll while the auto-refresh switch is on.
-  useEffect(() => {
-    if (!autoRefresh) return;
-    const timer = setInterval(() => {
-      void loadData();
-    }, 5000);
-    return () => clearInterval(timer);
-  }, [autoRefresh, loadData]);
-
   const handleRestartBroker = async (broker: BrokerRecord) => {
     try {
       const result = await restartBroker(broker.clusterId, broker.brokerName);
@@ -202,7 +209,10 @@ const BrokerClusterPage = () => {
     const timeoutId = window.setTimeout(() => {
       void loadData();
     });
-    return () => window.clearTimeout(timeoutId);
+    return () => {
+      window.clearTimeout(timeoutId);
+      ++loadRequestId.current;
+    };
   }, [loadData]);
 
   useEffect(() => {
@@ -222,6 +232,7 @@ const BrokerClusterPage = () => {
         color: 'error',
         label: t('brokerCluster.statusMaintenance'),
       },
+      unknown: { color: 'default', label: t('common.na') },
     };
     const { color, label } = config[status] || config.running;
     return <Tag color={color}>{label}</Tag>;
diff --git a/web/src/pages/studio/GroupManagement.tsx 
b/web/src/pages/studio/GroupManagement.tsx
index 9948beb9..4459e2d1 100644
--- a/web/src/pages/studio/GroupManagement.tsx
+++ b/web/src/pages/studio/GroupManagement.tsx
@@ -67,31 +67,54 @@ const GroupManagementPage = () => {
   const [progress, setProgress] = useState<QueueProgress[]>([]);
   const [detailLoading, setDetailLoading] = useState(false);
   const listRequestId = useRef(0);
+  const listInFlight = useRef<Promise<void> | null>(null);
+  const listRefreshQueued = useRef(false);
+  const mountedRef = useRef(true);
   const detailRequestId = useRef(0);
   const { t } = useLang();
 
-  const loadGroups = useCallback(async () => {
-    const requestId = ++listRequestId.current;
+  const loadGroups = useCallback((): Promise<void> => {
+    if (listInFlight.current) {
+      listRefreshQueued.current = true;
+      return listInFlight.current;
+    }
+
     setLoading(true);
-    try {
-      const data = await listConsumerGroups();
-      if (requestId !== listRequestId.current) return;
-      setGroups(data);
-    } catch {
-      if (requestId !== listRequestId.current) return;
-      message.error(t('consumer.fetchListFailed'));
-    } finally {
-      if (requestId === listRequestId.current) {
+    const run = async () => {
+      do {
+        listRefreshQueued.current = false;
+        const requestId = ++listRequestId.current;
+        try {
+          const data = await listConsumerGroups();
+          if (!mountedRef.current || requestId !== listRequestId.current) 
return;
+          setGroups(data);
+        } catch {
+          if (!mountedRef.current || requestId !== listRequestId.current) 
return;
+          message.error(t('consumer.fetchListFailed'));
+        }
+      } while (mountedRef.current && listRefreshQueued.current);
+    };
+
+    const cycle = run().finally(() => {
+      listInFlight.current = null;
+      if (mountedRef.current) {
         setLoading(false);
       }
-    }
+    });
+    listInFlight.current = cycle;
+    return cycle;
   }, [t]);
 
   useEffect(() => {
     const timeoutId = window.setTimeout(() => {
       void loadGroups();
     });
-    return () => window.clearTimeout(timeoutId);
+    return () => {
+      window.clearTimeout(timeoutId);
+      mountedRef.current = false;
+      listRefreshQueued.current = false;
+      ++listRequestId.current;
+    };
   }, [loadGroups]);
 
   useEffect(() => {
@@ -117,8 +140,8 @@ const GroupManagementPage = () => {
       setDetailLoading(true);
       try {
         const [subs, prog] = await Promise.all([
-          getConsumerSubscriptions(group.name),
-          getConsumerProgress(group.name),
+          getConsumerSubscriptions(group.name, group.instanceId),
+          getConsumerProgress(group.name, group.instanceId),
         ]);
         if (requestId !== detailRequestId.current) return;
         setSubscriptions(subs);
@@ -286,7 +309,7 @@ const GroupManagementPage = () => {
             size="small"
           />
           <Button icon={<ArrowClockwise size={14} />} size="small" 
onClick={handleRefresh}>
-            {t('common.reset')}
+            {t('common.refresh')}
           </Button>
         </Space>
       </div>
@@ -295,7 +318,9 @@ const GroupManagementPage = () => {
         <Table
           columns={columns}
           dataSource={filteredGroupData}
-          rowKey="name"
+          rowKey={(record) =>
+            `${record.instanceId || record.clusterId || 
'unscoped'}\0${record.name}`
+          }
           loading={loading}
           pagination={{
             pageSize: 10,
@@ -340,8 +365,13 @@ const GroupManagementPage = () => {
                           </div>
                           <div style={{ fontSize: 24, fontWeight: 600 }}>
                             {selectedGroup.onlineInstances}{' '}
-                            <Tag color="success" style={{ marginLeft: 8 }}>
-                              {t('groupMgmt.online')}
+                            <Tag
+                              color={selectedGroup.onlineInstances > 0 ? 
'success' : 'error'}
+                              style={{ marginLeft: 8 }}
+                            >
+                              {selectedGroup.onlineInstances > 0
+                                ? t('groupMgmt.online')
+                                : t('groupMgmt.stopped')}
                             </Tag>
                           </div>
                         </Card>
diff --git a/web/src/pages/studio/Ops.tsx b/web/src/pages/studio/Ops.tsx
index f5320c5d..d042f1d2 100644
--- a/web/src/pages/studio/Ops.tsx
+++ b/web/src/pages/studio/Ops.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import { Alert, App, Button, Input, Popconfirm, Select, Space, Switch, 
Tooltip, Typography } from 'antd';
 import { FloppyDisk, Plus, Trash } from '@phosphor-icons/react';
 import { useLang } from '../../i18n/LangContext';
@@ -42,6 +42,10 @@ const OpsPage: React.FC = () => {
   const [newNamesrvAddr, setNewNamesrvAddr] = useState('');
   const [useVIPChannel, setUseVIPChannel] = useState(false);
   const [useTLS, setUseTLS] = useState(false);
+  const [vipUpdating, setVipUpdating] = useState(false);
+  const [tlsUpdating, setTlsUpdating] = useState(false);
+  const vipUpdateInFlight = useRef(false);
+  const tlsUpdateInFlight = useRef(false);
   const [configurationAvailable, setConfigurationAvailable] = useState(false);
   const [unavailableReason, setUnavailableReason] = useState('');
   const writeOperationEnabled = configurationAvailable && (!token || admin === 
true);
@@ -121,6 +125,9 @@ const OpsPage: React.FC = () => {
   };
 
   const handleUpdateIsVIPChannel = async (checked: boolean) => {
+    if (vipUpdateInFlight.current) return;
+    vipUpdateInFlight.current = true;
+    setVipUpdating(true);
     setUseVIPChannel(checked);
     try {
       await updateIsVIPChannel(checked);
@@ -128,10 +135,16 @@ const OpsPage: React.FC = () => {
     } catch {
       message.error(t('common.failure'));
       setUseVIPChannel(!checked);
+    } finally {
+      vipUpdateInFlight.current = false;
+      setVipUpdating(false);
     }
   };
 
   const handleUpdateUseTLS = async (checked: boolean) => {
+    if (tlsUpdateInFlight.current) return;
+    tlsUpdateInFlight.current = true;
+    setTlsUpdating(true);
     setUseTLS(checked);
     try {
       await updateUseTLS(checked);
@@ -139,6 +152,9 @@ const OpsPage: React.FC = () => {
     } catch {
       message.error(t('common.failure'));
       setUseTLS(!checked);
+    } finally {
+      tlsUpdateInFlight.current = false;
+      setTlsUpdating(false);
     }
   };
 
@@ -215,13 +231,16 @@ const OpsPage: React.FC = () => {
           <Switch
             checked={useVIPChannel}
             onChange={handleUpdateIsVIPChannel}
-            disabled={!writeOperationEnabled}
+            disabled={!writeOperationEnabled || vipUpdating}
+            loading={vipUpdating}
           />
           {writeOperationEnabled && (
             <Button
               type="primary"
               icon={<FloppyDisk size={16} />}
               onClick={() => handleUpdateIsVIPChannel(useVIPChannel)}
+              loading={vipUpdating}
+              disabled={vipUpdating}
             >
               {t('common.update')}
             </Button>
@@ -236,13 +255,16 @@ const OpsPage: React.FC = () => {
           <Switch
             checked={useTLS}
             onChange={handleUpdateUseTLS}
-            disabled={!writeOperationEnabled}
+            disabled={!writeOperationEnabled || tlsUpdating}
+            loading={tlsUpdating}
           />
           {writeOperationEnabled && (
             <Button
               type="primary"
               icon={<FloppyDisk size={16} />}
               onClick={() => handleUpdateUseTLS(useTLS)}
+              loading={tlsUpdating}
+              disabled={tlsUpdating}
             >
               {t('common.update')}
             </Button>
diff --git a/web/src/pages/studio/Proxy.tsx b/web/src/pages/studio/Proxy.tsx
index 548c836f..7561e731 100644
--- a/web/src/pages/studio/Proxy.tsx
+++ b/web/src/pages/studio/Proxy.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 import {
   Card,
   Table,
@@ -63,6 +63,7 @@ const ProxyPage: React.FC = () => {
   const [configModalOpen, setConfigModalOpen] = useState(false);
   const [addNodeModalOpen, setAddNodeModalOpen] = useState(false);
   const [form] = Form.useForm();
+  const loadRequestId = useRef(0);
 
   const [clusterStats, setClusterStats] = useState({
     totalNodes: 0,
@@ -72,9 +73,11 @@ const ProxyPage: React.FC = () => {
   });
 
   const loadProxyNodes = useCallback(async () => {
+    const requestId = ++loadRequestId.current;
     setLoading(true);
     try {
       const { proxyAddrList, currentProxyAddr } = await queryProxyHomePage();
+      if (requestId !== loadRequestId.current) return false;
       const nodes: ProxyNode[] = (proxyAddrList || []).map((addr) => ({
         key: addr,
         address: addr,
@@ -103,10 +106,13 @@ const ProxyPage: React.FC = () => {
       }
       return true;
     } catch {
+      if (requestId !== loadRequestId.current) return false;
       message.error(t('proxy.fetchListFailed'));
       return false;
     } finally {
-      setLoading(false);
+      if (requestId === loadRequestId.current) {
+        setLoading(false);
+      }
     }
   }, [message, t]);
 
@@ -114,6 +120,9 @@ const ProxyPage: React.FC = () => {
     // The state updates are performed by the asynchronous Proxy API request, 
not by this effect itself.
     // eslint-disable-next-line react-hooks/set-state-in-effect
     void loadProxyNodes();
+    return () => {
+      ++loadRequestId.current;
+    };
   }, [loadProxyNodes]);
 
   const handleViewConfig = (node: ProxyNode) => {
@@ -121,43 +130,39 @@ const ProxyPage: React.FC = () => {
     setConfigModalOpen(true);
   };
 
-  const handleAddNode = () => {
-    form
-      .validateFields()
-      .then((values) => {
-        setLoading(true);
-        addProxyAddr(values.address)
-          .then(() => {
-            message.success(t('common.success'));
-            setAddNodeModalOpen(false);
-            form.resetFields();
-            loadProxyNodes();
-          })
-          .catch(() => {
-            message.error(t('proxy.addFailed'));
-          })
-          .finally(() => {
-            setLoading(false);
-          });
-      })
-      .catch(() => {
-        // validation failed
-      });
+  const handleAddNode = async () => {
+    let values: { address: string };
+    try {
+      values = await form.validateFields();
+    } catch {
+      return;
+    }
+
+    setLoading(true);
+    try {
+      await addProxyAddr(values.address);
+      message.success(t('common.success'));
+      setAddNodeModalOpen(false);
+      form.resetFields();
+      await loadProxyNodes();
+    } catch {
+      message.error(t('proxy.addFailed'));
+    } finally {
+      setLoading(false);
+    }
   };
 
-  const handleRemoveNode = (node: ProxyNode) => {
+  const handleRemoveNode = async (node: ProxyNode) => {
     setLoading(true);
-    removeProxyAddr(node.address)
-      .then(() => {
-        message.success(t('common.success'));
-        loadProxyNodes();
-      })
-      .catch(() => {
-        message.error(t('proxy.removeFailed'));
-      })
-      .finally(() => {
-        setLoading(false);
-      });
+    try {
+      await removeProxyAddr(node.address);
+      message.success(t('common.success'));
+      await loadProxyNodes();
+    } catch {
+      message.error(t('proxy.removeFailed'));
+    } finally {
+      setLoading(false);
+    }
   };
 
   const handleRefresh = async () => {
diff --git a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx 
b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
index 08c82779..9eb034a0 100644
--- a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
+++ b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
@@ -117,6 +117,14 @@ const renderWithProviders = (ui: React.ReactElement) => {
   );
 };
 
+const createDeferred = <T,>() => {
+  let resolve!: (value: T) => void;
+  const promise = new Promise<T>((resolvePromise) => {
+    resolve = resolvePromise;
+  });
+  return { promise, resolve };
+};
+
 describe('BrokerCluster Page', () => {
   beforeEach(() => {
     vi.clearAllMocks();
@@ -227,14 +235,75 @@ describe('BrokerCluster Page', () => {
     const liveRefreshSwitch = screen.getByRole('switch');
     fireEvent.click(liveRefreshSwitch);
     await act(async () => {
-      await vi.advanceTimersByTimeAsync(2000);
+      await vi.advanceTimersByTimeAsync(6000);
     });
-    expect(listClusters).toHaveBeenCalledTimes(2);
+    expect(listClusters).toHaveBeenCalledTimes(4);
 
     fireEvent.click(liveRefreshSwitch);
     await act(async () => {
       await vi.advanceTimersByTimeAsync(4000);
     });
-    expect(listClusters).toHaveBeenCalledTimes(2);
+    expect(listClusters).toHaveBeenCalledTimes(4);
   });
+  it('keeps the latest cluster list when an older refresh resolves last', 
async () => {
+    const older = createDeferred<ClusterInfo[]>();
+    const latest = createDeferred<ClusterInfo[]>();
+    vi.mocked(listClusters)
+      .mockResolvedValueOnce(clusterFixture)
+      .mockReturnValueOnce(older.promise)
+      .mockReturnValueOnce(latest.promise);
+    const user = userEvent.setup();
+    renderWithProviders(<BrokerCluster />);
+    await screen.findByText('broker-api-a');
+
+    const refreshButton = screen.getByText('重置');
+    await user.click(refreshButton);
+    await user.click(refreshButton);
+
+    const latestFixture = [
+      {
+        ...clusterFixture[0],
+        brokers: [{ ...clusterFixture[0].brokers[0], name: 'latest-broker' }],
+      },
+    ];
+    await act(async () => latest.resolve(latestFixture));
+    expect(await screen.findByText('latest-broker')).toBeInTheDocument();
+
+    await act(async () => older.resolve(clusterFixture));
+    expect(screen.getByText('latest-broker')).toBeInTheDocument();
+    expect(screen.queryByText('broker-api-a')).not.toBeInTheDocument();
+  });
+
+  it('formats bracketed IPv6 proxy gRPC endpoints without truncating the 
host', async () => {
+    vi.mocked(listClusters).mockResolvedValue([
+      {
+        ...clusterFixture[0],
+        proxies: [
+          {
+            ...clusterFixture[0].proxies[0],
+            addr: '[2001:db8::10]:8080',
+            grpcPort: 8081,
+          },
+        ],
+      },
+    ]);
+    const user = userEvent.setup();
+    renderWithProviders(<BrokerCluster />);
+    await screen.findByText('broker-api-a');
+    await user.click(screen.getByText('Proxy 管理'));
+
+    expect(screen.getByText('[2001:db8::10]:8081')).toBeInTheDocument();
+  });
+  it('renders unrecognized broker statuses as unavailable instead of running', 
async () => {
+    vi.mocked(listClusters).mockResolvedValue([{
+      ...clusterFixture[0],
+      brokers: [{ ...clusterFixture[0].brokers[0], status: 'mystery' }],
+    }]);
+    renderWithProviders(<BrokerCluster />);
+
+    await screen.findByText('broker-api-a');
+    expect(screen.getByText('N/A')).toBeInTheDocument();
+    expect(screen.queryByText('运行中')).not.toBeInTheDocument();
+  });
+
 });
diff --git a/web/src/pages/studio/__tests__/GroupManagement.test.tsx 
b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
index 4bc1f505..27068102 100644
--- a/web/src/pages/studio/__tests__/GroupManagement.test.tsx
+++ b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 
'vitest';
-import { act, fireEvent, render, screen, waitFor } from 
'@testing-library/react';
+import { act, fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import { LangProvider } from '../../../i18n/LangContext';
@@ -124,9 +124,9 @@ describe('GroupManagement Page', () => {
     expect(screen.queryByText('查看分布')).not.toBeInTheDocument();
   });
 
-  it('should render reset button', () => {
+  it('should render refresh button', () => {
     renderWithProviders(<GroupManagement />);
-    expect(screen.getByText('重置')).toBeInTheDocument();
+    expect(screen.getByText('刷新')).toBeInTheDocument();
   });
 
   it('should display consumer group data from the service in table', async () 
=> {
@@ -198,32 +198,24 @@ describe('GroupManagement Page', () => {
     expect(screen.queryByText('FIRST_GROUP_TOPIC')).not.toBeInTheDocument();
   });
 
-  it('keeps the latest group list when an earlier refresh resolves last', 
async () => {
+  it('queues one refresh instead of overlapping an active group request', 
async () => {
     const initialGroups = createDeferred<ConsumerGroup[]>();
     const refreshedGroups = createDeferred<ConsumerGroup[]>();
     vi.mocked(consumerService.listConsumerGroups)
       .mockReturnValueOnce(initialGroups.promise)
       .mockReturnValueOnce(refreshedGroups.promise);
-    vi.useFakeTimers();
     renderWithProviders(<GroupManagement />);
 
-    await act(async () => {
-      await vi.advanceTimersByTimeAsync(0);
-    });
-    fireEvent.click(screen.getByText('重置'));
+    await waitFor(() => 
expect(consumerService.listConsumerGroups).toHaveBeenCalledTimes(1));
+    fireEvent.click(screen.getByText('刷新'));
+    expect(consumerService.listConsumerGroups).toHaveBeenCalledTimes(1);
 
-    await act(async () => {
-      refreshedGroups.resolve([makeGroup({ name: 'fresh-group' })]);
-      await Promise.resolve();
-    });
-    expect(screen.getByText('fresh-group')).toBeInTheDocument();
+    initialGroups.resolve([makeGroup({ name: 'initial-group' })]);
+    await waitFor(() => 
expect(consumerService.listConsumerGroups).toHaveBeenCalledTimes(2));
 
-    await act(async () => {
-      initialGroups.resolve([makeGroup({ name: 'stale-group' })]);
-      await Promise.resolve();
-    });
-    expect(screen.getByText('fresh-group')).toBeInTheDocument();
-    expect(screen.queryByText('stale-group')).not.toBeInTheDocument();
+    refreshedGroups.resolve([makeGroup({ name: 'fresh-group' })]);
+    expect(await screen.findByText('fresh-group')).toBeInTheDocument();
+    expect(screen.queryByText('initial-group')).not.toBeInTheDocument();
   });
 
   it('polls only while auto refresh is enabled', async () => {
@@ -260,4 +252,54 @@ describe('GroupManagement Page', () => {
     expect(screen.getByText('order-consumer-group')).toBeInTheDocument();
     
expect(screen.queryByText('payment-consumer-group')).not.toBeInTheDocument();
   });
+  it('scopes global group detail diagnostics to the record instance', async () 
=> {
+    vi.mocked(consumerService.listConsumerGroups).mockResolvedValue([
+      makeGroup({ name: 'shared-group', instanceId: 'instance-b' }),
+    ]);
+    const user = userEvent.setup();
+    renderWithProviders(<GroupManagement />);
+    await screen.findByText('shared-group');
+    await user.click(screen.getByText('详情'));
+
+    await waitFor(() => {
+      expect(consumerService.getConsumerSubscriptions).toHaveBeenCalledWith(
+        'shared-group',
+        'instance-b',
+      );
+      expect(consumerService.getConsumerProgress).toHaveBeenCalledWith(
+        'shared-group',
+        'instance-b',
+      );
+    });
+  });
+
+  it('shows a stopped status in details when no consumer instance is online', 
async () => {
+    vi.mocked(consumerService.listConsumerGroups).mockResolvedValue([
+      makeGroup({ name: 'offline-group', onlineInstances: 0 }),
+    ]);
+    const user = userEvent.setup();
+    renderWithProviders(<GroupManagement />);
+    await screen.findByText('offline-group');
+    await user.click(screen.getByText('详情'));
+
+    const dialog = await screen.findByRole('dialog');
+    expect(within(dialog).getByText('已停止')).toBeInTheDocument();
+    expect(within(dialog).queryByText('在线')).not.toBeInTheDocument();
+  });
+
+  it('uses unique row keys for same-named groups from different instances', 
async () => {
+    vi.mocked(consumerService.listConsumerGroups).mockResolvedValue([
+      makeGroup({ name: 'shared-group', instanceId: 'instance-a' }),
+      makeGroup({ name: 'shared-group', instanceId: 'instance-b' }),
+    ]);
+    const { container } = renderWithProviders(<GroupManagement />);
+    await screen.findAllByText('shared-group');
+
+    const rowKeys = Array.from(container.querySelectorAll('tbody 
tr[data-row-key]')).map((row) =>
+      row.getAttribute('data-row-key'),
+    );
+    expect(rowKeys).toContain('instance-a\0shared-group');
+    expect(rowKeys).toContain('instance-b\0shared-group');
+    expect(new Set(rowKeys).size).toBe(rowKeys.length);
+  });
 });
diff --git a/web/src/pages/studio/__tests__/Ops.test.tsx 
b/web/src/pages/studio/__tests__/Ops.test.tsx
index beb12a8b..31c147da 100644
--- a/web/src/pages/studio/__tests__/Ops.test.tsx
+++ b/web/src/pages/studio/__tests__/Ops.test.tsx
@@ -22,7 +22,7 @@ import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import { LangProvider } from '../../../i18n/LangContext';
 import OpsPage from '../Ops';
-import { deleteNameSvrAddr, queryOpsHomePage } from '../../../api/ops';
+import { deleteNameSvrAddr, queryOpsHomePage, updateIsVIPChannel } from 
'../../../api/ops';
 import useAuthStore from '../../../stores/authStore';
 
 vi.mock('../../../api/ops', () => ({
@@ -85,6 +85,28 @@ describe('OpsPage', () => {
     expect(screen.getAllByRole('switch')[1]).not.toBeChecked();
   });
 
+  it('prevents overlapping VIP channel updates', async () => {
+    let resolveUpdate!: () => void;
+    vi.mocked(updateIsVIPChannel).mockReturnValue(
+      new Promise<void>((resolve) => {
+        resolveUpdate = resolve;
+      }),
+    );
+    renderWithProviders(<OpsPage />);
+
+    const vipSwitch = (await screen.findAllByRole('switch'))[0];
+    await waitFor(() => expect(vipSwitch).toBeChecked());
+    fireEvent.click(vipSwitch);
+    await waitFor(() => expect(updateIsVIPChannel).toHaveBeenCalledTimes(1));
+    expect(vipSwitch).toBeDisabled();
+
+    fireEvent.click(vipSwitch);
+    expect(updateIsVIPChannel).toHaveBeenCalledTimes(1);
+
+    resolveUpdate();
+    await waitFor(() => expect(vipSwitch).toBeEnabled());
+  });
+
   it('hides write controls for read-only users', async () => {
     useAuthStore.setState({ token: 'token-reader', user: 'reader', admin: 
false });
 
diff --git a/web/src/pages/studio/__tests__/Proxy.test.tsx 
b/web/src/pages/studio/__tests__/Proxy.test.tsx
index 7476c0d3..f67552f9 100644
--- a/web/src/pages/studio/__tests__/Proxy.test.tsx
+++ b/web/src/pages/studio/__tests__/Proxy.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { render, screen, waitFor } from '@testing-library/react';
+import { act, render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import { queryProxyHomePage } from '../../../api/proxy';
@@ -60,6 +60,14 @@ function renderPage() {
   );
 }
 
+const createDeferred = <T,>() => {
+  let resolve!: (value: T) => void;
+  const promise = new Promise<T>((resolvePromise) => {
+    resolve = resolvePromise;
+  });
+  return { promise, resolve };
+};
+
 describe('ProxyPage', () => {
   beforeEach(() => {
     vi.clearAllMocks();
@@ -119,4 +127,29 @@ describe('ProxyPage', () => {
     expect(screen.queryByText('5.3.0')).not.toBeInTheDocument();
     expect(screen.getAllByText('N/A').length).toBeGreaterThanOrEqual(5);
   });
+  it('keeps the latest Proxy list when an older refresh resolves last', async 
() => {
+    const older = createDeferred<typeof proxyHome>();
+    const latest = createDeferred<typeof proxyHome>();
+    vi.mocked(queryProxyHomePage)
+      .mockResolvedValueOnce(proxyHome)
+      .mockReturnValueOnce(older.promise)
+      .mockReturnValueOnce(latest.promise);
+    const user = userEvent.setup();
+    renderPage();
+    await screen.findByText('127.0.0.1:8081');
+
+    const refresh = screen.getByRole('button', { name: '刷新' });
+    await user.click(refresh);
+    await user.click(refresh);
+    await act(async () => latest.resolve({
+      proxyAddrList: ['127.0.0.2:8081'],
+      currentProxyAddr: '127.0.0.2:8081',
+    }));
+    expect(await screen.findByText('127.0.0.2:8081')).toBeInTheDocument();
+
+    await act(async () => older.resolve(proxyHome));
+    expect(screen.getByText('127.0.0.2:8081')).toBeInTheDocument();
+    expect(screen.queryByText('127.0.0.1:8081')).not.toBeInTheDocument();
+  });
+
 });

Reply via email to