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

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit f82c340a16560ffebac73e63e7c2175b5ed4161a
Author: Logic <[email protected]>
AuthorDate: Wed Aug 26 10:10:38 2026 +0800

    refactor: preserve shared React runtime refinements
---
 web-app/src/app/administrative-app-routes.test.tsx | 10 +++--
 .../app/refine/refine-resource-registry.test.ts    |  4 +-
 .../src/app/refine/refine-resource-registry.tsx    | 14 ++++---
 web-app/src/app/refine/refine-runtime.test.tsx     |  1 +
 web-app/src/app/refine/refine-runtime.tsx          | 20 ++++++++-
 .../src/app/refine/session-query-runtime.test.tsx  | 25 ++++++++++++
 web-app/src/app/refine/session-query-runtime.tsx   | 11 ++++-
 web-app/src/app/theme/hertzbeat-theme.test.ts      | 21 +++++++++-
 web-app/src/core/auth/session-cache-boundary.ts    | 17 ++++++++
 .../core/auth/session-foreground-revalidation.ts   | 21 ++--------
 .../auth/pages/login-page-style-contract.test.ts   | 26 +++++++++---
 .../src/features/auth/pages/login-page.module.css  | 47 ++++++++++++++--------
 .../monitor-definition-catalog.module.css          |  2 +-
 .../monitor-definition-layout-contract.test.ts     |  8 ++++
 .../controller/topology-query-keys.test.ts         |  2 +-
 .../topology/runtime/topology-g6-lifecycle.test.ts | 21 ++++++++++
 .../topology/runtime/topology-g6-lifecycle.ts      |  5 +++
 .../src/layout/shell/hertzbeat-shell.module.css    |  2 +-
 .../layout/shell/shell-navigation-style.test.ts    |  8 ++++
 .../notification-workspace-navigation.module.css   |  2 +-
 .../notification-workspace-style-contract.test.ts  |  7 ++--
 web-app/src/shared/theme/hertzbeat-theme.ts        | 24 +++++++++++
 22 files changed, 234 insertions(+), 64 deletions(-)

diff --git a/web-app/src/app/administrative-app-routes.test.tsx 
b/web-app/src/app/administrative-app-routes.test.tsx
index f8fe189657..799daf2f23 100644
--- a/web-app/src/app/administrative-app-routes.test.tsx
+++ b/web-app/src/app/administrative-app-routes.test.tsx
@@ -137,12 +137,14 @@ describe('actual administrative app routes', () => {
     await waitFor(() => expect(probes.pluginApi).toHaveBeenCalledOnce());
   });
 
-  it('admits ADMIN to the guarded Deployment loader and page API', async () => 
{
+  it('keeps the incomplete Deployment route unavailable to ADMIN without 
loading feature code or APIs', async () => {
     renderAppRoute('/settings/deployment', 'ADMIN');
 
-    expect(await screen.findByTestId('deployment-page')).toBeInTheDocument();
-    expect(probes.deploymentLoader).toHaveBeenCalledOnce();
-    await waitFor(() => expect(probes.deploymentApi).toHaveBeenCalledOnce());
+    expect(
+      (await 
screen.findByText(i18n.t('common.permission.additionalRequiredTitle'))).closest('[data-state]')
+    ).toHaveAttribute('data-state', 'permission');
+    expect(probes.deploymentLoader).not.toHaveBeenCalled();
+    expect(probes.deploymentApi).not.toHaveBeenCalled();
   });
 
   it('converges the legacy Plugin path on the guarded canonical route', async 
() => {
diff --git a/web-app/src/app/refine/refine-resource-registry.test.ts 
b/web-app/src/app/refine/refine-resource-registry.test.ts
index 6711ec55a5..eddf133780 100644
--- a/web-app/src/app/refine/refine-resource-registry.test.ts
+++ b/web-app/src/app/refine/refine-resource-registry.test.ts
@@ -72,7 +72,7 @@ describe('Refine shell resource registry', () => {
       'shell-administration'
     ]);
     expect(navigationChildren(tree, 
'shell-basic-monitoring')).toEqual(['monitors', 'bulletin', 
'monitor-definitions']);
-    expect(navigationChildren(tree, 
'shell-application-observability')).toEqual(['explore', 'instrumentation']);
+    expect(navigationChildren(tree, 
'shell-application-observability')).toEqual(['instrumentation', 'explore']);
     expect(navigationChildren(tree, 'shell-resources')).toEqual(['entities', 
'topology']);
     expect(navigationChildren(tree, 'shell-alerting')).toEqual([
       'alerts',
@@ -88,7 +88,6 @@ describe('Refine shell resource registry', () => {
     ]);
     expect(navigationChildren(tree, 'shell-administration')).toEqual([
       'system-config',
-      'deployment-settings',
       'plugins',
       'collectors',
       'tokens',
@@ -113,6 +112,7 @@ describe('Refine shell resource registry', () => {
     const names = flattenNavigationNames(buildShellNavigation(refineResources, 
['ADMIN']));
 
     expect(names).not.toContain('settings');
+    expect(names).not.toContain('deployment-settings');
     expect(names.some(name => 
name.startsWith('monitor-category:'))).toBe(false);
     expect(names.some(name => name.startsWith('monitor-app:'))).toBe(false);
   });
diff --git a/web-app/src/app/refine/refine-resource-registry.tsx 
b/web-app/src/app/refine/refine-resource-registry.tsx
index 01be7ef074..fbf400712a 100644
--- a/web-app/src/app/refine/refine-resource-registry.tsx
+++ b/web-app/src/app/refine/refine-resource-registry.tsx
@@ -162,17 +162,17 @@ const staticRefineResources: ResourceProps[] = [
     order: 20,
     timePolicy: 'global'
   }),
-  routedNavigationResource('explore', {
+  routedNavigationResource('instrumentation', {
     parent: 'shell-application-observability',
-    icon: <AreaChartOutlined />,
+    icon: <ApiOutlined />,
     order: 10,
-    timePolicy: 'route_owned'
+    timePolicy: 'none'
   }),
-  routedNavigationResource('instrumentation', {
+  routedNavigationResource('explore', {
     parent: 'shell-application-observability',
-    icon: <ApiOutlined />,
+    icon: <AreaChartOutlined />,
     order: 20,
-    timePolicy: 'none'
+    timePolicy: 'route_owned'
   }),
   routedNavigationResource('alerts', {
     parent: 'shell-alerting',
@@ -288,6 +288,8 @@ const staticRefineResources: ResourceProps[] = [
   routedNavigationResource('deployment-settings', {
     parent: 'shell-administration',
     icon: <DeploymentUnitOutlined />,
+    capability: 'unsupported',
+    navigation: false,
     order: 15,
     timePolicy: 'none'
   }),
diff --git a/web-app/src/app/refine/refine-runtime.test.tsx 
b/web-app/src/app/refine/refine-runtime.test.tsx
index d1226fe6b4..986342f153 100644
--- a/web-app/src/app/refine/refine-runtime.test.tsx
+++ b/web-app/src/app/refine/refine-runtime.test.tsx
@@ -144,6 +144,7 @@ describe('production Refine runtime', () => {
     expect(mountedClients).toContain(observedClient);
     expect(mountedClients.filter(client => 
client.getQueryData(sessionQueryKey))).toEqual([observedClient]);
     
expect(observedClient.getQueryData(sessionQueryKey)).toEqual(authenticatedSession);
+    expect(monitorApi.loadMonitorNavigationApps).toHaveBeenCalledTimes(1);
   });
 });
 
diff --git a/web-app/src/app/refine/refine-runtime.tsx 
b/web-app/src/app/refine/refine-runtime.tsx
index f525918fb6..781a1c8b65 100644
--- a/web-app/src/app/refine/refine-runtime.tsx
+++ b/web-app/src/app/refine/refine-runtime.tsx
@@ -19,7 +19,7 @@ import { Refine } from '@refinedev/core';
 import { useNotificationProvider } from '@refinedev/antd';
 import routerProvider from '@refinedev/react-router';
 import { QueryClient } from '@tanstack/react-query';
-import { useMemo, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import { Outlet } from 'react-router-dom';
 
 import { SessionProvider } from '@/core/auth/session-provider';
@@ -87,6 +87,9 @@ function RefineGenerationRuntime({
   queryClient: QueryClient;
 }) {
   const [monitorApps, setMonitorApps] = useState<readonly MonitorApp[]>([]);
+  const updateMonitorApps = useCallback((next: readonly MonitorApp[]) => {
+    setMonitorApps(current => (sameMonitorApps(current, next) ? current : 
next));
+  }, []);
   const resources = useMemo(() => buildRefineResources(monitorApps), 
[monitorApps]);
   return (
     <Refine
@@ -104,9 +107,22 @@ function RefineGenerationRuntime({
       }}
     >
       <SessionProvider>
-        <MonitorNavigationResourceLoader onChange={setMonitorApps} />
+        <MonitorNavigationResourceLoader onChange={updateMonitorApps} />
         <Outlet />
       </SessionProvider>
     </Refine>
   );
 }
+
+function sameMonitorApps(current: readonly MonitorApp[], next: readonly 
MonitorApp[]) {
+  return (
+    current.length === next.length &&
+    current.every(
+      (app, index) =>
+        app.category === next[index]?.category &&
+        app.value === next[index]?.value &&
+        app.label === next[index]?.label &&
+        app.hide === next[index]?.hide
+    )
+  );
+}
diff --git a/web-app/src/app/refine/session-query-runtime.test.tsx 
b/web-app/src/app/refine/session-query-runtime.test.tsx
index a3fbcbcea1..fb394af70a 100644
--- a/web-app/src/app/refine/session-query-runtime.test.tsx
+++ b/web-app/src/app/refine/session-query-runtime.test.tsx
@@ -232,6 +232,31 @@ describe('SessionQueryRuntime', () => {
     expect(fetchMock).toHaveBeenCalledTimes(2);
   });
 
+  it('keeps the current QueryClient when a safe-read refresh confirms the same 
identity', async () => {
+    const clients: QueryClient[] = [];
+    sessionApi.refreshSession.mockResolvedValue({ ...userA, roles: 
[...userA.roles] });
+    const fetchMock = vi
+      .fn<typeof fetch>()
+      .mockResolvedValueOnce(new Response(null, { status: 401 }))
+      .mockResolvedValueOnce(new Response('{}', { status: 200 }));
+    vi.stubGlobal('fetch', fetchMock);
+    renderRuntime(clients);
+    fireEvent.click(screen.getByRole('button', { name: 'Publish user A' }));
+    const currentClient = clients.at(-1);
+    currentClient?.setQueryData(['protected', 'workspace-a'], 'operator-a');
+    convergence.broadcast.mockClear();
+
+    await expect(apiFetch('/api/protected')).resolves.toMatchObject({ status: 
200 });
+
+    expect(sessionApi.refreshSession).toHaveBeenCalledOnce();
+    expect(clients).toHaveLength(2);
+    expect(clients.at(-1)).toBe(currentClient);
+    expect(currentClient?.getQueryData(sessionQueryKey)).toEqual(userA);
+    expect(currentClient?.getQueryData(['protected', 
'workspace-a'])).toBe('operator-a');
+    expect(convergence.broadcast).not.toHaveBeenCalled();
+    expect(fetchMock).toHaveBeenCalledTimes(2);
+  });
+
   it('converges a failed safe-read refresh to a new anonymous QueryClient', 
async () => {
     const clients: QueryClient[] = [];
     sessionApi.refreshSession.mockRejectedValue(new 
SessionRequestError('error', { status: 200 }));
diff --git a/web-app/src/app/refine/session-query-runtime.tsx 
b/web-app/src/app/refine/session-query-runtime.tsx
index e6cb920078..ca7dfde9d8 100644
--- a/web-app/src/app/refine/session-query-runtime.tsx
+++ b/web-app/src/app/refine/session-query-runtime.tsx
@@ -19,7 +19,11 @@ import type { QueryClient } from '@tanstack/react-query';
 import { useCallback, useLayoutEffect, useRef, useState, type ReactNode } from 
'react';
 import { flushSync } from 'react-dom';
 
-import { createCheckingSessionQueryClient, createSessionQueryClient } from 
'@/core/auth/session-cache-boundary';
+import {
+  createCheckingSessionQueryClient,
+  createSessionQueryClient,
+  hasSessionIdentityBoundaryChanged
+} from '@/core/auth/session-cache-boundary';
 import { createSessionConvergenceChannel } from 
'@/core/auth/session-convergence-channel';
 import {
   anonymousSession,
@@ -137,7 +141,10 @@ function useSessionRefresh(
           if (!mountedRef.current || runtimeRef.current.generation !== 
generation) {
             return { status: 'retired' } satisfies BrowserSessionRefreshResult;
           }
-          replaceIdentity(refreshedSession, options);
+          const currentSession = 
runtimeRef.current.queryClient.getQueryData<UiSession>(sessionQueryKey);
+          if (hasSessionIdentityBoundaryChanged(currentSession, 
refreshedSession)) {
+            replaceIdentity(refreshedSession, options);
+          }
           return {
             status: refreshedSession.authenticated ? 'renewed' : 'rejected'
           } satisfies BrowserSessionRefreshResult;
diff --git a/web-app/src/app/theme/hertzbeat-theme.test.ts 
b/web-app/src/app/theme/hertzbeat-theme.test.ts
index 9d86d46b69..f5781ab1c2 100644
--- a/web-app/src/app/theme/hertzbeat-theme.test.ts
+++ b/web-app/src/app/theme/hertzbeat-theme.test.ts
@@ -16,6 +16,13 @@ describe('HertzBeat semantic theme', () => {
 
     expect(theme.token).toMatchObject({
       colorPrimary: '#9b5bb3',
+      colorError: '#e15b5d',
+      colorErrorText: '#e15b5d',
+      colorErrorTextActive: '#e15b5d',
+      colorErrorTextHover: '#e46a6b',
+      colorLink: '#b97bca',
+      colorLinkActive: '#b97bca',
+      colorLinkHover: '#c18ad0',
       colorBgElevated: '#14171e',
       borderRadius: 6,
       borderRadiusLG: 8,
@@ -29,6 +36,9 @@ describe('HertzBeat semantic theme', () => {
     });
     expect(theme.components?.Button).toMatchObject({
       borderRadius: 5,
+      colorError: '#e15b5d',
+      colorErrorActive: '#e15b5d',
+      colorErrorHover: '#e46a6b',
       controlHeight: 32,
       defaultShadow: 'none',
       fontWeight: 600,
@@ -68,7 +78,16 @@ describe('HertzBeat semantic theme', () => {
       components: { Menu: { itemSelectedBg: '#211a26' } }
     });
     expect(createHertzBeatTheme('default')).toMatchObject({
-      token: { colorBgElevated: '#ffffff' },
+      token: {
+        colorBgElevated: '#ffffff',
+        colorError: '#b42318',
+        colorErrorText: '#b42318',
+        colorErrorTextActive: '#8f1c13',
+        colorErrorTextHover: '#9f2117',
+        colorLink: '#71357f',
+        colorLinkActive: '#512459',
+        colorLinkHover: '#63306f'
+      },
       components: {
         Input: { hoverBorderColor: '#89919e' },
         Menu: { itemSelectedBg: '#f7f0f8' },
diff --git a/web-app/src/core/auth/session-cache-boundary.ts 
b/web-app/src/core/auth/session-cache-boundary.ts
index 265dd70fed..85a9fe0d68 100644
--- a/web-app/src/core/auth/session-cache-boundary.ts
+++ b/web-app/src/core/auth/session-cache-boundary.ts
@@ -32,3 +32,20 @@ export function createSessionQueryClient(createQueryClient: 
() => QueryClient, n
 export function createCheckingSessionQueryClient(createQueryClient: () => 
QueryClient) {
   return createQueryClient();
 }
+
+export function hasSessionIdentityBoundaryChanged(current: UiSession | 
undefined, next: UiSession) {
+  return (
+    current === undefined ||
+    current.authenticated !== next.authenticated ||
+    current.username !== next.username ||
+    current.workspaceId !== next.workspaceId ||
+    current.expiresAt !== next.expiresAt ||
+    !haveSameRoles(current.roles, next.roles)
+  );
+}
+
+function haveSameRoles(current: string[], next: string[]) {
+  if (current.length !== next.length) return false;
+  const nextRoles = new Set(next);
+  return current.every(role => nextRoles.has(role));
+}
diff --git a/web-app/src/core/auth/session-foreground-revalidation.ts 
b/web-app/src/core/auth/session-foreground-revalidation.ts
index b33ccba438..798205e190 100644
--- a/web-app/src/core/auth/session-foreground-revalidation.ts
+++ b/web-app/src/core/auth/session-foreground-revalidation.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 
+import { hasSessionIdentityBoundaryChanged } from './session-cache-boundary';
 import { anonymousSession, getSession, SessionRequestError, type UiSession } 
from './session-api';
 import type { ReplaceSessionIdentity } from './session-identity-context';
 
@@ -64,7 +65,9 @@ export function startForegroundSessionRevalidation({
     try {
       const nextSession = await getSession({ signal });
       if (!ownsCurrentGeneration(owner.generation)) return;
-      if (hasSessionIdentityBoundaryChanged(owner.session, nextSession)) 
replaceIdentity(nextSession);
+      if (hasSessionIdentityBoundaryChanged(owner.session, nextSession)) {
+        replaceIdentity(nextSession);
+      }
     } catch (reason) {
       if (!ownsCurrentGeneration(owner.generation) || signal.aborted) return;
       if (isAuthoritativeSessionRejection(reason)) 
replaceIdentity(anonymousSession);
@@ -91,22 +94,6 @@ export function startForegroundSessionRevalidation({
   };
 }
 
-function hasSessionIdentityBoundaryChanged(current: UiSession, next: 
UiSession) {
-  return (
-    current.authenticated !== next.authenticated ||
-    current.username !== next.username ||
-    current.workspaceId !== next.workspaceId ||
-    current.expiresAt !== next.expiresAt ||
-    !haveSameRoles(current.roles, next.roles)
-  );
-}
-
-function haveSameRoles(current: string[], next: string[]) {
-  if (current.length !== next.length) return false;
-  const nextRoles = new Set(next);
-  return current.every(role => nextRoles.has(role));
-}
-
 function isAuthoritativeSessionRejection(reason: unknown) {
   return reason instanceof SessionRequestError && (reason.status === 401 || 
reason.status === 403);
 }
diff --git a/web-app/src/features/auth/pages/login-page-style-contract.test.ts 
b/web-app/src/features/auth/pages/login-page-style-contract.test.ts
index 30b0952039..afcd56721e 100644
--- a/web-app/src/features/auth/pages/login-page-style-contract.test.ts
+++ b/web-app/src/features/auth/pages/login-page-style-contract.test.ts
@@ -10,24 +10,25 @@ import { describe, expect, it } from 'vitest';
 import stylesheet from './login-page.module.css?raw';
 
 describe('login page visual contract', () => {
-  it('uses the original artwork on desktop and preserves its light-theme 
treatment', () => {
+  it('uses the original artwork across passport breakpoints and preserves its 
light-theme treatment', () => {
     expect(stylesheet).toContain("url('/assets/bg.png')");
-    expect(stylesheet).toMatch(/@media \(min-width: 
960px\)[\s\S]*\.page\s*{[^}]*background-image:\s*url/s);
+    expect(stylesheet).toMatch(/\.page\s*{[^}]*background-image:\s*url/s);
+    expect(stylesheet).not.toMatch(/@media \(min-width: 
960px\)[\s\S]*\.page\s*{[^}]*background-image:\s*url/s);
     expect(stylesheet).not.toContain(":root[data-theme='default']");
     expect(stylesheet).toMatch(/@media \(min-width: 960px\)/);
   });
 
   it('keeps the form narrow, the explanation desktop-only, and the passport 
route responsive', () => {
     expect(stylesheet).toMatch(/\.formRegion\s*{[^}]*width:\s*min\(368px, 
100%\)/s);
-    expect(stylesheet).toMatch(/\.introductionBrand\s*{[^}]*width:\s*280px/s);
+    
expect(stylesheet).toMatch(/\.introductionBrand\s*{[^}]*width:\s*300px[^}]*height:\s*75px/s);
     expect(stylesheet).toMatch(/@media \(min-width: 
960px\)[\s\S]*\.brandHeader\s*{[^}]*display:\s*none/s);
     expect(stylesheet).toMatch(/@media \(max-width: 
959px\)[\s\S]*\.introduction\s*{[^}]*display:\s*none/s);
     
expect(stylesheet).toMatch(/body:has\(\[data-passport-page='true'\]\)[^}]*min-width:\s*0/s);
   });
 
   it('keeps each capability on one fitted line and rolls the completed line 
before typing the next', () => {
-    
expect(stylesheet).toMatch(/\.introductionPositioning\s*{[^}]*margin-bottom:\s*clamp\(26px,\s*2\.5vw,\s*32px\)/s);
-    
expect(stylesheet).toMatch(/\.introductionPhraseStage\s*{[^}]*block-size:\s*1\.45em/s);
+    
expect(stylesheet).toMatch(/\.introductionPositioning\s*{[^}]*margin-bottom:\s*clamp\(44px,\s*4vw,\s*56px\)/s);
+    
expect(stylesheet).toMatch(/\.introductionPhraseStage\s*{[^}]*block-size:\s*1\.55em[^}]*font-size:\s*36px/s);
     
expect(stylesheet).toMatch(/\.introductionTypingLine\s*{[^}]*white-space:\s*nowrap/s);
     expect(stylesheet).toContain('@keyframes passportIntroductionRollOut');
     expect(stylesheet).toContain('@keyframes passportIntroductionRollIn');
@@ -43,8 +44,21 @@ describe('login page visual contract', () => {
   it('uses a translucent glass login panel without a secondary introduction 
paragraph', () => {
     expect(stylesheet).not.toContain('.introductionDescription');
     expect(stylesheet).toMatch(
-      /@media \(min-width: 
960px\)[\s\S]*\.panel\s*{[^}]*background:\s*color-mix\(in srgb, 
var\(--hb-bg-raised\) 58%, transparent\)/s
+      /\.panel\s*{[^}]*background:\s*color-mix\(in srgb, var\(--hb-bg-raised\) 
58%, transparent\)/s
     );
     
expect(stylesheet).toMatch(/\.panel\s*{[^}]*backdrop-filter:\s*blur\(18px\) 
saturate\(1\.15\)/s);
   });
+
+  it('gives the desktop form enough vertical weight without widening it', () 
=> {
+    expect(stylesheet).toMatch(/@media \(min-width: 
960px\)[\s\S]*\.panel\s*{[^}]*padding:\s*36px/s);
+    expect(stylesheet).toMatch(
+      /@media \(min-width: 
960px\)[\s\S]*\.panel\s*>\s*:global\(h2\.ant-typography\)\s*{[^}]*margin-bottom:\s*22px/s
+    );
+    expect(stylesheet).toMatch(
+      /@media \(min-width: 
960px\)[\s\S]*\.panel\s+:global\(\.ant-input\)[^}]*min-height:\s*42px/s
+    );
+    expect(stylesheet).toMatch(
+      /@media \(min-width: 
960px\)[\s\S]*\.panel\s+:global\(\.ant-btn\)[^}]*min-height:\s*42px/s
+    );
+  });
 });
diff --git a/web-app/src/features/auth/pages/login-page.module.css 
b/web-app/src/features/auth/pages/login-page.module.css
index 6f006ae49e..a362283d7e 100644
--- a/web-app/src/features/auth/pages/login-page.module.css
+++ b/web-app/src/features/auth/pages/login-page.module.css
@@ -11,6 +11,10 @@
   padding: 40px clamp(24px, 5vw, 72px);
   overflow-x: hidden;
   background: var(--hb-bg-canvas);
+  background-image: url('/assets/bg.png');
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: cover;
 }
 
 .shell {
@@ -82,15 +86,15 @@
 
 .introductionBrand {
   display: block;
-  width: 280px;
-  height: 70px;
-  margin-bottom: 4px;
+  width: 300px;
+  height: 75px;
+  margin-bottom: 12px;
 }
 
 .introductionPositioning {
   display: block;
   max-width: 540px;
-  margin-bottom: clamp(26px, 2.5vw, 32px);
+  margin-bottom: clamp(44px, 4vw, 56px);
   font-size: clamp(16px, 1.45vw, 20px);
   font-weight: 520;
   letter-spacing: -0.01em;
@@ -101,9 +105,9 @@
   position: relative;
   display: block;
   width: 100%;
-  block-size: 1.45em;
+  block-size: 1.55em;
   overflow: hidden;
-  font-size: 34px;
+  font-size: 36px;
   line-height: 1.18;
 }
 
@@ -133,7 +137,7 @@
 .introductionMeasure {
   position: absolute;
   visibility: hidden;
-  font-size: 34px;
+  font-size: 36px;
   white-space: nowrap;
   pointer-events: none;
 }
@@ -190,10 +194,12 @@
 .panel {
   width: 100%;
   padding: 30px;
-  border: 1px solid var(--hb-border-subtle);
+  border: 1px solid color-mix(in srgb, var(--hb-bg-raised) 72%, transparent);
   border-radius: var(--hb-radius-surface);
-  background: var(--hb-bg-raised);
+  background: color-mix(in srgb, var(--hb-bg-raised) 58%, transparent);
   box-shadow: var(--hb-shadow-surface);
+  -webkit-backdrop-filter: blur(18px) saturate(1.15);
+  backdrop-filter: blur(18px) saturate(1.15);
 }
 
 .brand {
@@ -238,11 +244,7 @@
   }
 
   .page {
-    background-image: url('/assets/bg.png');
-    background-repeat: no-repeat;
-    background-position: center;
     background-attachment: fixed;
-    background-size: cover;
   }
 
   .introduction {
@@ -251,10 +253,21 @@
   }
 
   .panel {
-    border-color: color-mix(in srgb, var(--hb-bg-raised) 72%, transparent);
-    background: color-mix(in srgb, var(--hb-bg-raised) 58%, transparent);
-    -webkit-backdrop-filter: blur(18px) saturate(1.15);
-    backdrop-filter: blur(18px) saturate(1.15);
+    padding: 36px;
+  }
+
+  .panel > :global(h2.ant-typography) {
+    margin-bottom: 22px;
+  }
+
+  .panel :global(.ant-input),
+  .panel :global(.ant-input-affix-wrapper),
+  .panel :global(.ant-btn) {
+    min-height: 42px;
+  }
+
+  .panel :global(.ant-input-affix-wrapper .ant-input) {
+    min-height: auto;
   }
 }
 
diff --git 
a/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css
 
b/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css
index cfffab09b4..291e43edc9 100644
--- 
a/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css
+++ 
b/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css
@@ -30,7 +30,7 @@
 .item.itemSelected:hover {
   border-radius: 0;
   color: var(--hb-text-primary);
-  background: var(--hb-nav-selected);
+  background: transparent;
   box-shadow: inset 2px 0 0 var(--hb-brand-accent);
 }
 
diff --git 
a/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts
 
b/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts
index 48bed6caff..88ac9cbea0 100644
--- 
a/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts
+++ 
b/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts
@@ -41,6 +41,14 @@ describe('monitor definition split layout contract', () => {
     );
     
expect(pageStyles).toMatch(/@media\s*\(max-width:\s*760px\)[\s\S]*\.workspace\s*\{[^}]*overflow:\s*visible/);
   });
+
+  it('uses a line-only marker for the selected catalog definition', () => {
+    const selected = 
catalogStyles.match(/\.item\.itemSelected,[\s\S]*?\{[^}]*\}/)?.[0] ?? '';
+
+    expect(selected).toMatch(/box-shadow:\s*inset 2px 0 0 
var\(--hb-brand-accent\)/);
+    expect(selected).toMatch(/background:\s*transparent/);
+    expect(selected).not.toMatch(/background:\s*var\(--hb-nav-selected\)/);
+  });
 });
 
 function cssRule(source: string, name: string) {
diff --git 
a/web-app/src/features/topology/controller/topology-query-keys.test.ts 
b/web-app/src/features/topology/controller/topology-query-keys.test.ts
index 2f0f1e35da..0299a21cfa 100644
--- a/web-app/src/features/topology/controller/topology-query-keys.test.ts
+++ b/web-app/src/features/topology/controller/topology-query-keys.test.ts
@@ -22,7 +22,7 @@ describe('topology query keys', () => {
     const base = topologyQueryKeys.graph(fullQuery, 3);
     expect(base).toEqual([
       'topology',
-      { context: '["","","","","prod","",""]', window: '1000:2000', 
refreshRevision: 3 },
+      { context: '["","","","","","","prod","",""]', window: '1000:2000', 
refreshRevision: 3 },
       {
         focusEntityId: 10,
         depth: 2,
diff --git 
a/web-app/src/features/topology/runtime/topology-g6-lifecycle.test.ts 
b/web-app/src/features/topology/runtime/topology-g6-lifecycle.test.ts
new file mode 100644
index 0000000000..b1b5c37e63
--- /dev/null
+++ b/web-app/src/features/topology/runtime/topology-g6-lifecycle.test.ts
@@ -0,0 +1,21 @@
+/* Licensed to the Apache Software Foundation (ASF) under the Apache License, 
Version 2.0. */
+
+import { describe, expect, it } from 'vitest';
+
+import { removeGeneratedCanvasTabStops } from './topology-g6-lifecycle';
+
+describe('Topology G6 accessibility lifecycle', () => {
+  it('keeps generated drawing surfaces out of the positive tab order', () => {
+    const layer = document.createElement('div');
+    const interactiveCanvas = document.createElement('canvas');
+    const decorativeCanvas = document.createElement('canvas');
+    interactiveCanvas.tabIndex = 1;
+    decorativeCanvas.tabIndex = 1;
+    layer.append(interactiveCanvas, decorativeCanvas);
+
+    removeGeneratedCanvasTabStops(layer);
+
+    expect(interactiveCanvas).toHaveAttribute('tabindex', '-1');
+    expect(decorativeCanvas).toHaveAttribute('tabindex', '-1');
+  });
+});
diff --git a/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts 
b/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts
index 05ca1165ad..58a8301b46 100644
--- a/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts
+++ b/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts
@@ -89,6 +89,7 @@ export async function initializeCandidateGraph(
   bindTopologyInteractionEvents(graph, module, resources.input, ownsGraph);
   resources.observer = observeSize(container, graph);
   await graph.render();
+  removeGeneratedCanvasTabStops(layer);
   if (cancelled()) return;
   const previousViewport = refs.graph.current ? 
readTopologyViewport(refs.graph.current) : refs.viewport.current;
   await restoreOrFitTopologyGraph(graph, previousViewport);
@@ -156,6 +157,10 @@ function activateGraphLayer(layer: HTMLDivElement) {
   layer.style.visibility = 'visible';
 }
 
+export function removeGeneratedCanvasTabStops(layer: HTMLElement) {
+  for (const canvas of layer.querySelectorAll('canvas')) canvas.tabIndex = -1;
+}
+
 function observeSize(container: HTMLDivElement, graph: TopologyG6Graph) {
   if (typeof ResizeObserver === 'undefined') return undefined;
   const observer = new ResizeObserver(entries => {
diff --git a/web-app/src/layout/shell/hertzbeat-shell.module.css 
b/web-app/src/layout/shell/hertzbeat-shell.module.css
index 2b0cd2f7f7..332423729a 100644
--- a/web-app/src/layout/shell/hertzbeat-shell.module.css
+++ b/web-app/src/layout/shell/hertzbeat-shell.module.css
@@ -365,7 +365,7 @@ button.timePolicy:hover {
   border-left-color: var(--hb-brand-accent);
   border-radius: 0;
   color: var(--hb-text-primary);
-  background: var(--hb-nav-selected);
+  background: transparent;
   font-weight: 600;
 }
 
diff --git a/web-app/src/layout/shell/shell-navigation-style.test.ts 
b/web-app/src/layout/shell/shell-navigation-style.test.ts
index d3ed2b4289..3d0b650508 100644
--- a/web-app/src/layout/shell/shell-navigation-style.test.ts
+++ b/web-app/src/layout/shell/shell-navigation-style.test.ts
@@ -22,4 +22,12 @@ describe('shell navigation visual hierarchy', () => {
   it('gives expanded first-level areas a restrained shared open state', () => {
     
expect(shellStyles).toMatch(/\.navigationParentOpen\s*\{[^}]*background:\s*var\(--hb-nav-hover\)/);
   });
+
+  it('marks the active destination with a line instead of a filled selection 
block', () => {
+    const activeRule = 
shellStyles.match(/\.navigationLinkActive\s*\{[^}]*\}/)?.[0] ?? '';
+
+    
expect(activeRule).toMatch(/border-left-color:\s*var\(--hb-brand-accent\)/);
+    expect(activeRule).toMatch(/background:\s*transparent/);
+    expect(activeRule).not.toMatch(/background:\s*var\(--hb-nav-selected\)/);
+  });
 });
diff --git 
a/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css
 
b/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css
index f43c1cadeb..b4b7a5f504 100644
--- 
a/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css
+++ 
b/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css
@@ -41,7 +41,7 @@
 
 .current a {
   border-left-color: var(--hb-brand-accent);
-  background: var(--hb-nav-selected);
+  background: transparent;
 }
 
 .index {
diff --git 
a/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts
 
b/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts
index baf7569b76..a84ed078d3 100644
--- 
a/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts
+++ 
b/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts
@@ -21,9 +21,10 @@ describe('notification workspace layout contract', () => {
     expect(dependencyRule).not.toMatch(/overflow:\s*hidden/);
   });
 
-  it('uses semantic hover and selected surfaces instead of the page canvas', 
() => {
+  it('uses a hover surface and a line-only current-step marker', () => {
     
expect(styles).toMatch(/\.steps\s+a:hover\s*\{[^}]*background:\s*var\(--hb-bg-hover\)/s);
-    
expect(styles).toMatch(/\.current\s+a\s*\{[^}]*background:\s*var\(--hb-nav-selected\)/s);
-    
expect(styles).not.toMatch(/\.current\s+a\s*\{[^}]*background:\s*var\(--hb-bg-canvas\)/s);
+    
expect(styles).toMatch(/\.current\s+a\s*\{[^}]*border-left-color:\s*var\(--hb-brand-accent\)/s);
+    
expect(styles).toMatch(/\.current\s+a\s*\{[^}]*background:\s*transparent/s);
+    
expect(styles).not.toMatch(/\.current\s+a\s*\{[^}]*background:\s*var\(--hb-nav-selected\)/s);
   });
 });
diff --git a/web-app/src/shared/theme/hertzbeat-theme.ts 
b/web-app/src/shared/theme/hertzbeat-theme.ts
index 5a823644d0..c82bab986a 100644
--- a/web-app/src/shared/theme/hertzbeat-theme.ts
+++ b/web-app/src/shared/theme/hertzbeat-theme.ts
@@ -33,6 +33,13 @@ function themeTokens(palette: HertzBeatPalette): 
NonNullable<ThemeConfig['token'
     colorBgElevated: palette.raised,
     colorBorder: palette.border,
     colorBorderSecondary: palette.border,
+    colorError: palette.error,
+    colorErrorText: palette.errorText,
+    colorErrorTextActive: palette.errorTextActive,
+    colorErrorTextHover: palette.errorTextHover,
+    colorLink: palette.link,
+    colorLinkActive: palette.linkActive,
+    colorLinkHover: palette.linkHover,
     colorPrimary: '#9b5bb3',
     colorPrimaryActive: '#7f448f',
     colorPrimaryHover: '#a96abd',
@@ -54,6 +61,9 @@ function controlComponents(palette: HertzBeatPalette): 
NonNullable<ThemeConfig['
   return {
     Button: {
       borderRadius: 5,
+      colorError: palette.errorText,
+      colorErrorActive: palette.errorTextActive,
+      colorErrorHover: palette.errorTextHover,
       controlHeight: 32,
       defaultActiveBorderColor: palette.activeBorder,
       defaultActiveColor: palette.text,
@@ -133,8 +143,15 @@ const darkPalette = {
   border: '#282d38',
   canvas: '#0d0f14',
   chrome: '#101218',
+  error: '#e15b5d',
+  errorText: '#e15b5d',
+  errorTextActive: '#e15b5d',
+  errorTextHover: '#e46a6b',
   muted: '#929aaa',
   hover: '#181b22',
+  link: '#b97bca',
+  linkActive: '#b97bca',
+  linkHover: '#c18ad0',
   raised: '#14171e',
   selected: '#211a26',
   selectedText: '#f4edf6',
@@ -148,8 +165,15 @@ const lightPalette = {
   border: '#dfe3e8',
   canvas: '#f5f6f8',
   chrome: '#ffffff',
+  error: '#b42318',
+  errorText: '#b42318',
+  errorTextActive: '#8f1c13',
+  errorTextHover: '#9f2117',
   muted: '#697180',
   hover: '#f1f2f5',
+  link: '#71357f',
+  linkActive: '#512459',
+  linkHover: '#63306f',
   raised: '#ffffff',
   selected: '#f7f0f8',
   selectedText: '#71357f',


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to