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 c64801e73d6171a82c41339c1a5b77d8307c2d40
Author: Logic <[email protected]>
AuthorDate: Fri May 29 01:14:54 2026 +0800

    feat(web-next): restore passport parity
---
 web-next/app/api/account/auth/form/route.ts        |  35 ++
 web-next/app/api/account/auth/refresh/route.ts     |  44 +++
 web-next/app/api/account/session/route.ts          |  24 ++
 web-next/app/login/page.tsx                        |   4 +-
 .../app/passport/lock/page-submit-flow.test.tsx    | 120 ++++++
 web-next/app/passport/lock/page.test.tsx           |  39 +-
 web-next/app/passport/lock/page.tsx                |  73 +---
 web-next/app/passport/lock/passport-lock-page.tsx  |  63 +++
 web-next/app/passport/login/page.test.tsx          |  25 +-
 web-next/app/passport/login/page.tsx               |  15 +-
 .../pages/login-form-submit-flow.test.tsx          | 421 +++++++++++++++++++++
 .../components/pages/login-form.submit.test.tsx    |  15 +-
 web-next/components/pages/login-form.test.tsx      |  86 +++--
 web-next/components/pages/login-form.tsx           | 203 ++++++----
 .../pages/passport-shell-session-clear.test.tsx    | 125 ++++++
 web-next/components/pages/passport-shell.test.tsx  |  47 ++-
 web-next/components/pages/passport-shell.tsx       |  27 +-
 web-next/lib/passport-lock/view-model.test.ts      |   6 +-
 web-next/lib/passport-lock/view-model.ts           |   4 +-
 web-next/lib/passport-login/controller.test.ts     | 108 +++++-
 web-next/lib/passport-login/controller.ts          |  92 ++++-
 web-next/lib/passport-login/view-model.test.ts     |  29 +-
 web-next/lib/passport-login/view-model.ts          |  20 +-
 web-next/lib/session-bff.test.ts                   | 178 +++++++++
 web-next/lib/session-bff.ts                        | 206 ++++++++++
 25 files changed, 1771 insertions(+), 238 deletions(-)

diff --git a/web-next/app/api/account/auth/form/route.ts 
b/web-next/app/api/account/auth/form/route.ts
new file mode 100644
index 0000000000..2192593721
--- /dev/null
+++ b/web-next/app/api/account/auth/form/route.ts
@@ -0,0 +1,35 @@
+import { NextRequest, NextResponse } from 'next/server';
+import {
+  applySessionCookies,
+  buildBackendApiUrl,
+  clearSessionCookies,
+  readJsonPayload,
+  sanitizeSessionPayload
+} from '@/lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+export async function POST(request: NextRequest) {
+  const upstream = await fetch(buildBackendApiUrl('/account/auth/form'), {
+    method: 'POST',
+    headers: {
+      'Content-Type': request.headers.get('Content-Type') || 
'application/json',
+      ...(request.headers.get('Accept-Language') ? { 'Accept-Language': 
request.headers.get('Accept-Language') as string } : {})
+    },
+    body: await request.text(),
+    cache: 'no-store'
+  });
+  const payload = await readJsonPayload(upstream);
+  const response = NextResponse.json(sanitizeSessionPayload(payload), { 
status: upstream.status });
+
+  if (upstream.ok && payload.code === 0 && payload.data) {
+    applySessionCookies(response, {
+      token: typeof payload.data.token === 'string' ? payload.data.token : 
undefined,
+      refreshToken: typeof payload.data.refreshToken === 'string' ? 
payload.data.refreshToken : undefined
+    });
+  } else {
+    clearSessionCookies(response);
+  }
+
+  return response;
+}
diff --git a/web-next/app/api/account/auth/refresh/route.ts 
b/web-next/app/api/account/auth/refresh/route.ts
new file mode 100644
index 0000000000..f55252ab21
--- /dev/null
+++ b/web-next/app/api/account/auth/refresh/route.ts
@@ -0,0 +1,44 @@
+import { NextRequest, NextResponse } from 'next/server';
+import {
+  HB_UI_REFRESH_COOKIE,
+  applySessionCookies,
+  buildBackendApiUrl,
+  clearSessionCookies,
+  readJsonPayload,
+  readSessionCookieValue,
+  sanitizeSessionPayload
+} from '@/lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+export async function POST(request: NextRequest) {
+  const refreshToken = readSessionCookieValue(request, HB_UI_REFRESH_COOKIE);
+  if (!refreshToken) {
+    const response = NextResponse.json({ code: 401, msg: 'Missing refresh 
session', data: null }, { status: 401 });
+    clearSessionCookies(response);
+    return response;
+  }
+
+  const upstream = await fetch(buildBackendApiUrl('/account/auth/refresh'), {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      ...(request.headers.get('Accept-Language') ? { 'Accept-Language': 
request.headers.get('Accept-Language') as string } : {})
+    },
+    body: JSON.stringify({ token: refreshToken }),
+    cache: 'no-store'
+  });
+  const payload = await readJsonPayload(upstream);
+  const response = NextResponse.json(sanitizeSessionPayload(payload), { 
status: upstream.status });
+
+  if (upstream.ok && payload.code === 0 && payload.data) {
+    applySessionCookies(response, {
+      token: typeof payload.data.token === 'string' ? payload.data.token : 
undefined,
+      refreshToken: typeof payload.data.refreshToken === 'string' ? 
payload.data.refreshToken : refreshToken
+    });
+  } else {
+    clearSessionCookies(response);
+  }
+
+  return response;
+}
diff --git a/web-next/app/api/account/session/route.ts 
b/web-next/app/api/account/session/route.ts
new file mode 100644
index 0000000000..dabd4ce88e
--- /dev/null
+++ b/web-next/app/api/account/session/route.ts
@@ -0,0 +1,24 @@
+import { NextRequest, NextResponse } from 'next/server';
+import {
+  HB_UI_ACCESS_COOKIE,
+  HB_UI_REFRESH_COOKIE,
+  clearSessionCookies,
+  readSessionCookieValue
+} from '@/lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET(request: NextRequest) {
+  return NextResponse.json({
+    authenticated: Boolean(
+      readSessionCookieValue(request, HB_UI_ACCESS_COOKIE) ||
+      readSessionCookieValue(request, HB_UI_REFRESH_COOKIE)
+    )
+  });
+}
+
+export async function DELETE() {
+  const response = NextResponse.json({ authenticated: false });
+  clearSessionCookies(response);
+  return response;
+}
diff --git a/web-next/app/login/page.tsx b/web-next/app/login/page.tsx
index cba692aacc..6d0a0f3833 100644
--- a/web-next/app/login/page.tsx
+++ b/web-next/app/login/page.tsx
@@ -1,5 +1,5 @@
 import { redirect } from 'next/navigation';
-import { buildCompatRedirectTarget, type SearchParamsRecord } from 
'../../lib/compat/search-params';
+import { buildLoginCompatRouteUrl, type SearchParamsRecord } from 
'../../lib/passport-login/controller';
 
 export default async function LoginAliasPage({
   searchParams
@@ -7,5 +7,5 @@ export default async function LoginAliasPage({
   searchParams?: Promise<SearchParamsRecord>;
 }) {
   const resolvedSearchParams = await searchParams;
-  redirect(buildCompatRedirectTarget('/passport/login', resolvedSearchParams));
+  redirect(buildLoginCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/passport/lock/page-submit-flow.test.tsx 
b/web-next/app/passport/lock/page-submit-flow.test.tsx
new file mode 100644
index 0000000000..2fb20d1495
--- /dev/null
+++ b/web-next/app/passport/lock/page-submit-flow.test.tsx
@@ -0,0 +1,120 @@
+// @vitest-environment jsdom
+
+import React from 'react';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import { HB_UI_SESSION_USER_KEY } from '../../../lib/session-client';
+
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean 
}).IS_REACT_ACT_ENVIRONMENT = true;
+
+const mockState = vi.hoisted(() => ({
+  routerReplace: vi.fn()
+}));
+
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: mockState.routerReplace
+  })
+}));
+
+vi.mock('next/image', () => ({
+  default: ({ alt, src, priority: _priority, ...props }: any) => 
React.createElement('img', { alt, src, ...props })
+}));
+
+vi.mock('@/components/providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({ locale: 'zh-CN' }),
+    locale: 'zh-CN',
+    locales: [
+      { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 
'gb' },
+      { code: 'zh-CN', labelKey: 'settings.system-config.locale.zh_CN', abbr: 
'cn' }
+    ],
+    setLocale: vi.fn()
+  })
+}));
+
+describe('passport lock submit flow', () => {
+  let container: HTMLDivElement;
+  let root: Root;
+
+  async function mountPage() {
+    const { default: PassportLockPage } = await import('./passport-lock-page');
+    await act(async () => {
+      root.render(<PassportLockPage />);
+      await Promise.resolve();
+    });
+  }
+
+  function setPassword(value: string) {
+    const input = 
container.querySelector('[data-hz-passport-lock-password-input="shared"]') as 
HTMLInputElement | null;
+    if (!input) throw new Error('Missing lock password input');
+    const descriptor = 
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+    descriptor?.set?.call(input, value);
+    input.dispatchEvent(new Event('input', { bubbles: true }));
+    input.dispatchEvent(new Event('change', { bubbles: true }));
+  }
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+    mockState.routerReplace.mockReset();
+  });
+
+  afterEach(() => {
+    act(() => {
+      root.unmount();
+    });
+    container.remove();
+    document.cookie = 'hb_ui_session=; Max-Age=0; path=/';
+  });
+
+  it('blocks empty unlock drafts and navigates to the workbench after a 
password is entered', async () => {
+    window.sessionStorage.setItem(
+      HB_UI_SESSION_USER_KEY,
+      JSON.stringify({ name: 'ops-admin', avatar: './assets/img/avatar.svg', 
email: 'administrator', role: 'ADMIN' })
+    );
+    document.cookie = 'hb_ui_session=1; path=/';
+    await mountPage();
+
+    
expect(container.querySelector('[data-hz-passport-lock-submit-lifecycle="angular-mark-dirty-required-then-dashboard"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-lock-required-mode-contract="angular-required-no-trim"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-lock-required-mode="angular-required-no-trim"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-session-clear-contract="angular-lock-preserve-session"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-session-clear-enabled="false"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-lock-avatar-contract="angular-settings-user-avatar"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-lock-avatar-source="settings-user-avatar"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-lock-avatar-img="settings-user-avatar"]')).not.toBeNull();
+    
expect((container.querySelector('[data-hz-passport-lock-avatar-img="settings-user-avatar"]')
 as HTMLImageElement).getAttribute('src')).toBe('./assets/img/avatar.svg');
+    
expect((container.querySelector('[data-hz-passport-lock-avatar-img="settings-user-avatar"]')
 as HTMLImageElement).getAttribute('alt')).toBe('ops-admin');
+    
expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).toContain('"name":"ops-admin"');
+    expect(document.cookie).toContain('hb_ui_session=1');
+    
expect(container.querySelector('[data-hz-passport-lock-submit-state="disabled"]')).not.toBeNull();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).not.toHaveBeenCalled();
+
+    await act(async () => {
+      setPassword('   ');
+      await Promise.resolve();
+    });
+
+    
expect(container.querySelector('[data-hz-passport-lock-submit-state="ready"]')).not.toBeNull();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenCalledWith('/overview');
+  }, 30000);
+});
diff --git a/web-next/app/passport/lock/page.test.tsx 
b/web-next/app/passport/lock/page.test.tsx
index a58a5bf10f..d5fe6d9548 100644
--- a/web-next/app/passport/lock/page.test.tsx
+++ b/web-next/app/passport/lock/page.test.tsx
@@ -10,12 +10,12 @@ vi.mock('next/navigation', () => ({
 }));
 
 vi.mock('next/image', () => ({
-  default: ({ alt, src, priority: _priority, ...props }: any) => <img 
alt={alt} src={src} {...props} />
+  default: ({ alt, src, priority: _priority, ...props }: any) => 
React.createElement('img', { alt, src, ...props })
 }));
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
+    t: createTranslatorMock({ locale: 'zh-CN' }),
     locale: 'zh-CN',
     locales: [
       { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 
'🇬🇧' },
@@ -26,11 +26,15 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 }));
 
 vi.mock('../../../components/pages/passport-shell', () => ({
-  PassportShell: ({ children, panelClassName }: any) => (
+  PassportShell: ({ children, panelClassName, sessionLifecycle }: any) => (
     <div
       data-login-shell="passport"
       data-passport-shell="true"
       data-passport-shell-panel-class={panelClassName}
+      data-passport-session-clear-contract={sessionLifecycle === 
'preserve-on-lock' ? 'angular-lock-preserve-session' : 
'angular-token-service-clear-on-passport-entry'}
+      data-passport-session-clear-enabled={sessionLifecycle === 
'preserve-on-lock' ? 'false' : 'true'}
+      data-hz-passport-session-clear-lifecycle={sessionLifecycle === 
'preserve-on-lock' ? 'angular-lock-preserve-session' : 
'angular-token-service-clear-on-passport-entry'}
+      data-hz-passport-session-clear-scope={sessionLifecycle === 
'preserve-on-lock' ? 'client-marker-user-snapshot-preserved' : 
'client-marker-user-snapshot'}
       style={{ backgroundImage: "url('/assets/bg.png')" }}
     >
       <div>Apache HertzBeat™</div>
@@ -63,14 +67,37 @@ describe('passport lock page', () => {
     const html = renderToStaticMarkup(<PassportLockPage />);
 
     expect(html).toContain('data-passport-shell="true"');
+    
expect(html).toContain('data-passport-session-clear-contract="angular-lock-preserve-session"');
+    expect(html).toContain('data-passport-session-clear-enabled="false"');
+    
expect(html).toContain('data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"');
+    
expect(html).toContain('data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"');
     expect(html).toContain('data-passport-panel="true"');
     expect(html).toContain('data-passport-lock="true"');
     expect(html).toContain('data-passport-lock-panel="angular-wide"');
+    
expect(html).toContain('data-passport-lock-panel-owner="hertzbeat-ui-passport-lock"');
+    
expect(html).toContain('data-passport-lock-avatar-contract="angular-settings-user-avatar"');
+    
expect(html).toContain('data-passport-lock-session-contract="angular-lock-preserve-session"');
+    
expect(html).toContain('data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"');
+    
expect(html).toContain('data-passport-lock-submit-lifecycle-owner="hertzbeat-ui-passport-lock"');
+    
expect(html).toContain('data-passport-lock-redirect-contract="angular-dashboard-next-overview"');
+    
expect(html).toContain('data-passport-lock-required-mode-contract="angular-required-no-trim"');
+    
expect(html).toContain('data-passport-lock-required-mode-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-hz-ui="passport-lock-surface"');
+    
expect(html).toContain('data-hz-passport-lock-owner="hertzbeat-ui-passport-lock"');
+    
expect(html).toContain('data-hz-passport-lock-density="angular-lock-card"');
+    
expect(html).toContain('data-hz-passport-lock-submit-lifecycle="angular-mark-dirty-required-then-dashboard"');
+    expect(html).toContain('data-hz-passport-lock-required-fields="password"');
+    
expect(html).toContain('data-hz-passport-lock-required-mode="angular-required-no-trim"');
+    
expect(html).toContain('data-hz-passport-lock-redirect="angular-dashboard-next-overview"');
+    
expect(html).toContain('data-hz-passport-lock-submit-disabled="angular-invalid-disabled"');
+    expect(html).toContain('data-hz-passport-lock-avatar="angular-floating"');
+    
expect(html).toContain('data-hz-passport-lock-avatar-source="fallback-user-icon"');
+    expect(html).toContain('data-hz-passport-lock-submit-state="disabled"');
     expect(html).toContain('max-w-[712px]');
     expect(html).toContain('rounded-none');
-    expect(html).toContain('Unlock');
-    expect(html).toContain('Enter Any To Unlock');
+    expect(html).toContain('解除锁定');
+    expect(html).toContain('输入任意解锁');
     expect(html).toContain('/assets/bg.png');
     expect(html).toContain('Apache HertzBeat™');
-  }, 15000);
+  }, 30000);
 });
diff --git a/web-next/app/passport/lock/page.tsx 
b/web-next/app/passport/lock/page.tsx
index baccd086db..25d490a279 100644
--- a/web-next/app/passport/lock/page.tsx
+++ b/web-next/app/passport/lock/page.tsx
@@ -1,71 +1,6 @@
-'use client';
+import React from 'react';
+import PassportLockPage from './passport-lock-page';
 
-import React, { FormEvent, useState } from 'react';
-import { useRouter } from 'next/navigation';
-import { LockKeyhole, UserRound } from 'lucide-react';
-import { ObservabilityStatusState } from '@/components/observability';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { validateUnlockPassword } from '../../../lib/passport-lock/view-model';
-import { PassportPanel, PassportShell } from 
'../../../components/pages/passport-shell';
-
-export default function PassportLockPage() {
-  const { t } = useI18n();
-  const router = useRouter();
-  const [password, setPassword] = useState('');
-  const [error, setError] = useState<string | null>(null);
-
-  function handleSubmit(event: FormEvent) {
-    event.preventDefault();
-    const validationError = validateUnlockPassword(password, t);
-    if (validationError) {
-      setError(validationError);
-      return;
-    }
-    setError(null);
-    router.replace('/overview');
-  }
-
-  return (
-    <PassportShell panelClassName="max-w-[712px] lg:max-w-[712px]">
-      <PassportPanel
-        className="mx-auto min-h-[334px] max-w-[712px] rounded-none 
border-[var(--ops-border-color)] bg-[#101217] px-6 py-14 shadow-none 
lg:px-[120px]"
-      >
-        <div data-passport-lock-panel="angular-wide" className="mx-auto flex 
min-h-[220px] max-w-[320px] flex-col justify-center">
-          <div className="text-center">
-            <div className="mx-auto flex h-16 w-16 items-center justify-center 
rounded-full bg-white/75 text-[var(--ops-surface-base)] shadow-none">
-              <UserRound size={28} />
-            </div>
-            <div className="mt-6 text-[16px] font-semibold 
text-[var(--ops-text-primary)]">{t('app.lock')}</div>
-          </div>
-          <form onSubmit={handleSubmit} className="mt-6 space-y-5" 
data-passport-lock="true">
-            <div className="relative">
-              <LockKeyhole
-                size={16}
-                className="pointer-events-none absolute left-3 top-1/2 
-translate-y-1/2 text-[var(--ops-text-tertiary)]"
-              />
-              <Input
-                placeholder={t('app.lock.placeholder')}
-                type="password"
-                value={password}
-                onChange={e => setPassword(e.target.value)}
-                className="h-9 border-[var(--ops-border-color)] 
bg-[var(--ops-surface-base)] pl-10"
-              />
-            </div>
-            <div className="flex justify-end">
-              <Button className="min-w-[76px]" size="sm" variant="primary" 
type="submit">
-                {t('app.lock')}
-              </Button>
-            </div>
-          </form>
-          {error ? (
-            <div className="mt-4">
-              <ObservabilityStatusState title={t('common.failed')} 
copy={error} tone="danger" />
-            </div>
-          ) : null}
-        </div>
-      </PassportPanel>
-    </PassportShell>
-  );
+export default function PassportLockRoutePage() {
+  return <PassportLockPage />;
 }
diff --git a/web-next/app/passport/lock/passport-lock-page.tsx 
b/web-next/app/passport/lock/passport-lock-page.tsx
new file mode 100644
index 0000000000..a6efdaf5d0
--- /dev/null
+++ b/web-next/app/passport/lock/passport-lock-page.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import React, { FormEvent, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { HzPassportLockSurface } from '@hertzbeat/ui';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { validateUnlockPassword } from '../../../lib/passport-lock/view-model';
+import { readClientSessionUserSnapshot } from '../../../lib/session-client';
+import { PassportPanel, PassportShell } from 
'../../../components/pages/passport-shell';
+
+export default function PassportLockPage() {
+  const { t } = useI18n();
+  const router = useRouter();
+  const [sessionUser] = useState(() => readClientSessionUserSnapshot());
+  const [password, setPassword] = useState('');
+  const [error, setError] = useState<string | null>(null);
+
+  function handleSubmit(event: FormEvent) {
+    event.preventDefault();
+    const validationError = validateUnlockPassword(password, t);
+    if (validationError) {
+      setError(validationError);
+      return;
+    }
+    setError(null);
+    router.replace('/overview');
+  }
+
+  return (
+    <PassportShell panelClassName="max-w-[712px] lg:max-w-[712px]" 
sessionLifecycle="preserve-on-lock">
+      <PassportPanel
+        className="mx-auto min-h-[334px] max-w-[712px] rounded-none 
border-[var(--ops-border-color)] bg-[#101217] px-6 py-14 shadow-none 
lg:px-[120px]"
+      >
+        <HzPassportLockSurface
+          title={t('app.lock')}
+          passwordLabel={t('app.lock.placeholder')}
+          passwordPlaceholder={t('app.lock.placeholder')}
+          buttonLabel={t('app.lock')}
+          password={password}
+          avatarSrc={sessionUser?.avatar}
+          avatarAlt={sessionUser?.name || t('app.lock')}
+          error={error}
+          disabled={password.length === 0}
+          onPasswordChange={value => {
+            setPassword(value);
+            if (error) setError(null);
+          }}
+          onSubmit={handleSubmit}
+          data-passport-lock="true"
+          data-passport-lock-panel="angular-wide"
+          data-passport-lock-panel-owner="hertzbeat-ui-passport-lock"
+          data-passport-lock-avatar-contract="angular-settings-user-avatar"
+          data-passport-lock-session-contract="angular-lock-preserve-session"
+          
data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"
+          
data-passport-lock-submit-lifecycle-owner="hertzbeat-ui-passport-lock"
+          
data-passport-lock-redirect-contract="angular-dashboard-next-overview"
+          data-passport-lock-required-mode-contract="angular-required-no-trim"
+          data-passport-lock-required-mode-owner="hertzbeat-ui-passport-lock"
+        />
+      </PassportPanel>
+    </PassportShell>
+  );
+}
diff --git a/web-next/app/passport/login/page.test.tsx 
b/web-next/app/passport/login/page.test.tsx
index 667793e281..369bc386a0 100644
--- a/web-next/app/passport/login/page.test.tsx
+++ b/web-next/app/passport/login/page.test.tsx
@@ -1,17 +1,38 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import type { PassportLoginRouteState } from '@/lib/passport-login/controller';
 
 vi.mock('@/components/pages/login-form', () => ({
-  LoginForm: () => <div data-login-form="true">passport login form</div>
+  LoginForm: ({ initialRouteState }: { initialRouteState?: 
PassportLoginRouteState }) => (
+    <div data-login-form="true" 
data-redirect-target={initialRouteState?.redirectTarget}>
+      passport login form
+    </div>
+  )
 }));
 
 describe('passport login page', () => {
   it('renders the shared login form surface', async () => {
     const { default: PassportLoginPage } = await import('./page');
-    const html = renderToStaticMarkup(<PassportLoginPage />);
+    const html = renderToStaticMarkup(await PassportLoginPage());
 
     expect(html).toContain('data-login-form="true"');
     expect(html).toContain('passport login form');
+    expect(html).toContain('data-redirect-target="/"');
+  });
+
+  it('passes the normalized redirect route state to the login form', async () 
=> {
+    const { default: PassportLoginPage } = await import('./page');
+    const { LoginForm } = await import('@/components/pages/login-form');
+    const initialRouteState = { redirectTarget: '/monitors?app=website' };
+    const mockedFormHtml = renderToStaticMarkup(<LoginForm 
initialRouteState={initialRouteState} />);
+    const html = renderToStaticMarkup(
+      await PassportLoginPage({
+        searchParams: Promise.resolve({ redirect: '/monitors?app=website' })
+      })
+    );
+
+    
expect(mockedFormHtml).toContain('data-redirect-target="/monitors?app=website"');
+    expect(html).toContain('data-redirect-target="/monitors?app=website"');
   });
 });
diff --git a/web-next/app/passport/login/page.tsx 
b/web-next/app/passport/login/page.tsx
index dcd4ae0942..3cc7e68f41 100644
--- a/web-next/app/passport/login/page.tsx
+++ b/web-next/app/passport/login/page.tsx
@@ -1,10 +1,21 @@
 import React, { Suspense } from 'react';
 import { LoginForm } from '@/components/pages/login-form';
+import {
+  readPassportLoginRouteState,
+  type PassportLoginSearchParams
+} from '@/lib/passport-login/controller';
+
+export default async function PassportLoginPage({
+  searchParams
+}: {
+  searchParams?: Promise<PassportLoginSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readPassportLoginRouteState(resolvedSearchParams);
 
-export default function PassportLoginPage() {
   return (
     <Suspense fallback={null}>
-      <LoginForm />
+      <LoginForm initialRouteState={routeState} />
     </Suspense>
   );
 }
diff --git a/web-next/components/pages/login-form-submit-flow.test.tsx 
b/web-next/components/pages/login-form-submit-flow.test.tsx
new file mode 100644
index 0000000000..4dcc42ac01
--- /dev/null
+++ b/web-next/components/pages/login-form-submit-flow.test.tsx
@@ -0,0 +1,421 @@
+// @vitest-environment jsdom
+
+import React from 'react';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+import type { PassportLoginRouteState } from 
'../../lib/passport-login/controller';
+
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean 
}).IS_REACT_ACT_ENVIRONMENT = true;
+
+const mockState = vi.hoisted(() => ({
+  resetWorkbenchLoadCache: vi.fn(),
+  routerReplace: vi.fn()
+}));
+
+const setLocale = vi.fn(async () => {});
+const t = createTranslatorMock();
+
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: mockState.routerReplace
+  })
+}));
+
+vi.mock('next/image', () => ({
+  default: ({ alt, src, priority: _priority, ...props }: any) => 
React.createElement('img', { alt, src, ...props })
+}));
+
+vi.mock('../providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t,
+    locale: 'en-US',
+    locales: [
+      { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 
'gb' },
+      { code: 'zh-CN', labelKey: 'settings.system-config.locale.zh_CN', abbr: 
'cn' },
+      { code: 'ja-JP', labelKey: 'settings.system-config.locale.ja-JP', abbr: 
'jp' }
+    ],
+    setLocale
+  })
+}));
+
+vi.mock('../ui/button', () => ({
+  Button: ({ children, ...props }: any) => <button 
{...props}>{children}</button>
+}));
+
+vi.mock('../ui/input', () => ({
+  Input: (props: any) => <input {...props} />
+}));
+
+vi.mock('../../lib/workbench-load-cache', () => ({
+  resetWorkbenchLoadCache: mockState.resetWorkbenchLoadCache
+}));
+
+describe('login form submit copy', () => {
+  let container: HTMLDivElement;
+  let root: Root;
+
+  async function mountForm(initialRouteState: PassportLoginRouteState = { 
redirectTarget: '/' }) {
+    const { LoginForm } = await import('./login-form');
+    await act(async () => {
+      root.render(<LoginForm initialRouteState={initialRouteState} />);
+      await Promise.resolve();
+    });
+  }
+
+  function setInputValue(index: number, value: string) {
+    const input = container.querySelectorAll('input')[index] as 
HTMLInputElement | undefined;
+    if (!input) throw new Error(`Missing input index ${index}`);
+    const descriptor = 
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+    descriptor?.set?.call(input, value);
+    input.dispatchEvent(new Event('input', { bubbles: true }));
+    input.dispatchEvent(new Event('change', { bubbles: true }));
+  }
+
+  async function flushAsyncWork(turns = 8) {
+    for (let index = 0; index < turns; index += 1) {
+      await Promise.resolve();
+    }
+  }
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+    mockState.resetWorkbenchLoadCache.mockReset();
+    mockState.routerReplace.mockReset();
+    setLocale.mockClear();
+    window.localStorage.clear();
+    window.sessionStorage.clear();
+    vi.stubGlobal('fetch', vi.fn());
+  });
+
+  afterEach(() => {
+    act(() => {
+      root.unmount();
+    });
+    container.remove();
+    vi.unstubAllGlobals();
+  });
+
+  it('uses the BFF cookie session without writing access or refresh tokens to 
localStorage', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { authenticated: true, 
tokenBoundary: 'bff-cookie', role: 'ADMIN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { locale: 'zh_CN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: [] })
+      });
+
+    await mountForm({ redirectTarget: '/monitors?app=website' });
+
+    
expect(container.querySelector('[data-hz-ui="passport-login-action-frame"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-token-boundary="bff-cookie-no-localstorage"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-session-bootstrap="angular-startup-load-after-success"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-session-user-name="angular-raw-identifier"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-startup-failure="angular-exception-500"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-login-startup-failure-contract="angular-exception-500"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-login-session-user-name-contract="angular-raw-identifier"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-required-mode="angular-required-no-trim"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-login-required-mode-contract="angular-required-no-trim"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-redirect="angular-referrer-non-passport-fallback"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-redirect-fallback="angular-root-fallback"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-login-redirect-fallback-contract="angular-root-fallback"]')).not.toBeNull();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/account/auth/form',
+      expect.objectContaining({
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ type: 0, identifier: 'ops-admin', credential: 
'custom-secret' })
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/config/system',
+      expect.objectContaining({
+        cache: 'no-store',
+        credentials: 'same-origin',
+        headers: expect.not.objectContaining({
+          Authorization: expect.any(String)
+        })
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/apps/hierarchy?lang=zh-CN',
+      expect.objectContaining({
+        cache: 'no-store',
+        credentials: 'same-origin',
+        headers: expect.not.objectContaining({
+          Authorization: expect.any(String)
+        })
+      })
+    );
+    expect(window.localStorage.getItem('Authorization')).toBeNull();
+    expect(window.localStorage.getItem('refresh-token')).toBeNull();
+    
expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"name":"ops-admin"');
+    
expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"role":"ADMIN"');
+    
expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).not.toContain('token');
+    
expect(window.sessionStorage.getItem('HB_ABOUT_AUTO_SHOW_AFTER_LOGIN')).toBe('true');
+    expect(mockState.resetWorkbenchLoadCache).toHaveBeenCalledTimes(1);
+    
expect(mockState.routerReplace).toHaveBeenCalledWith('/monitors?app=website');
+  }, 60000);
+
+  it('routes to the Angular startup failure page when post-login hierarchy 
bootstrap fails', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { authenticated: true, 
tokenBoundary: 'bff-cookie', role: 'ADMIN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { locale: 'zh_CN' } })
+      })
+      .mockRejectedValueOnce(new Error('hierarchy failed'));
+
+    await mountForm({ redirectTarget: '/monitors?app=website' });
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/apps/hierarchy?lang=zh-CN',
+      expect.objectContaining({
+        cache: 'no-store',
+        credentials: 'same-origin'
+      })
+    );
+    
expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"name":"ops-admin"');
+    
expect(window.sessionStorage.getItem('HB_ABOUT_AUTO_SHOW_AFTER_LOGIN')).toBeNull();
+    expect(mockState.routerReplace).toHaveBeenCalledWith('/exception/500');
+    
expect(mockState.routerReplace).not.toHaveBeenCalledWith('/monitors?app=website');
+  }, 60000);
+
+  it('keeps Angular default-password warning as the first submit instead of 
posting immediately', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'admin');
+      setInputValue(1, 'hertzbeat');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    
expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-notice-link="account-modify"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-default-password="angular-first-submit-warning"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-default-password-lifecycle="angular-sticky-until-submit"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"]')).not.toBeNull();
+  }, 60000);
+
+  it('keeps the Angular default-password notice sticky after the credential 
changes', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock.mockResolvedValueOnce({
+      ok: false,
+      status: 401,
+      json: async () => ({ code: 1, msg: 'bad credential' })
+    });
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'admin');
+      setInputValue(1, 'hertzbeat');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    
expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+
+    await act(async () => {
+      setInputValue(1, 'custom-secret');
+      await flushAsyncWork();
+    });
+
+    
expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenCalledTimes(1);
+    
expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+    expect(container.textContent).toContain('bad credential');
+  }, 60000);
+
+  it('keeps Angular credential required-field validation on the client before 
posting', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+
+    await mountForm();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    expect(container.textContent).toContain('Please enter your username');
+    
expect(container.querySelector('[data-hz-ui="passport-login-validation-notice"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-login-validation-density="angular-error-alert"]')).not.toBeNull();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    expect(container.textContent).toContain('Please enter password');
+  }, 60000);
+
+  it('allows Angular required whitespace credentials through without trimming 
before posting', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock.mockResolvedValueOnce({
+      ok: false,
+      status: 401,
+      json: async () => ({ code: 1, msg: 'bad credential' })
+    });
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, '   ');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenCalledTimes(1);
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/account/auth/form',
+      expect.objectContaining({
+        body: JSON.stringify({ type: 0, identifier: 'ops-admin', credential: ' 
  ' })
+      })
+    );
+    expect(container.textContent).toContain('bad credential');
+  }, 60000);
+
+  it('keeps Angular post-login user snapshot name as the raw submitted 
identifier', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { authenticated: true, 
tokenBoundary: 'bff-cookie', role: 'ADMIN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { locale: 'zh_CN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: [] })
+      });
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, ' ops-admin ');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/account/auth/form',
+      expect.objectContaining({
+        body: JSON.stringify({ type: 0, identifier: ' ops-admin ', credential: 
'custom-secret' })
+      })
+    );
+    
expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"name":" 
ops-admin "');
+    expect(mockState.routerReplace).toHaveBeenCalledWith('/');
+  }, 60000);
+
+  it('renders localized loading and fallback login errors', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    let resolveLoginResponse: ((value: unknown) => void) | undefined;
+    const loginResponse = new Promise(resolve => {
+      resolveLoginResponse = resolve;
+    });
+    fetchMock.mockReturnValueOnce(loginResponse);
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect((container.querySelector('button[type="submit"]') as 
HTMLButtonElement | null)?.textContent).toContain('Logging in');
+
+    await act(async () => {
+      resolveLoginResponse?.({
+        ok: false,
+        status: 503,
+        json: async () => ({ code: 1 })
+      });
+      await loginResponse;
+      await flushAsyncWork(12);
+    });
+
+    expect(container.textContent).toContain('Login failed: 503');
+    expect(mockState.routerReplace).not.toHaveBeenCalled();
+  }, 60000);
+
+  it('renders the localized generic fallback when login throws a non-Error 
value', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock.mockRejectedValueOnce('network-cancelled');
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new 
Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(container.textContent).toContain('Login failed. Check username or 
password.');
+    expect(mockState.routerReplace).not.toHaveBeenCalled();
+  }, 60000);
+});
diff --git a/web-next/components/pages/login-form.submit.test.tsx 
b/web-next/components/pages/login-form.submit.test.tsx
index f347798c83..28f21a60cd 100644
--- a/web-next/components/pages/login-form.submit.test.tsx
+++ b/web-next/components/pages/login-form.submit.test.tsx
@@ -90,13 +90,13 @@ describe('login form submit flow', () => {
     vi.unstubAllGlobals();
   });
 
-  it('persists tokens, warms bootstrap config, and restores the guarded return 
path after login', async () => {
+  it('uses the BFF cookie session, warms bootstrap config, and restores the 
guarded return path after login', async () => {
     const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
     fetchMock
       .mockResolvedValueOnce({
         ok: true,
         status: 200,
-        json: async () => ({ code: 0, data: { token: 'access-token', 
refreshToken: 'refresh-token' } })
+        json: async () => ({ code: 0, data: { authenticated: true, 
tokenBoundary: 'bff-cookie' } })
       })
       .mockResolvedValueOnce({
         ok: true,
@@ -130,13 +130,14 @@ describe('login form submit flow', () => {
       '/api/config/system',
       expect.objectContaining({
         cache: 'no-store',
-        headers: expect.objectContaining({
-          Authorization: 'Bearer access-token'
+        credentials: 'same-origin',
+        headers: expect.not.objectContaining({
+          Authorization: expect.any(String)
         })
       })
     );
-    expect(window.localStorage.getItem('Authorization')).toBe('access-token');
-    expect(window.localStorage.getItem('refresh-token')).toBe('refresh-token');
+    expect(window.localStorage.getItem('Authorization')).toBeNull();
+    expect(window.localStorage.getItem('refresh-token')).toBeNull();
     
expect(mockState.routerReplace).toHaveBeenCalledWith('/monitors?app=website');
   }, 15000);
 
@@ -146,7 +147,7 @@ describe('login form submit flow', () => {
       .mockResolvedValueOnce({
         ok: true,
         status: 200,
-        json: async () => ({ code: 0, data: { token: 'access-token', 
refreshToken: 'refresh-token' } })
+        json: async () => ({ code: 0, data: { authenticated: true, 
tokenBoundary: 'bff-cookie' } })
       })
       .mockResolvedValueOnce({
         ok: true,
diff --git a/web-next/components/pages/login-form.test.tsx 
b/web-next/components/pages/login-form.test.tsx
index 0b0a9bc157..fb4100d697 100644
--- a/web-next/components/pages/login-form.test.tsx
+++ b/web-next/components/pages/login-form.test.tsx
@@ -6,36 +6,35 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
-const ZH_CN_LABEL = 'Simplified Chinese(zh_CN)';
+const ZH_CN_LABEL = '简体中文(zh_CN)';
 const JA_JP_LABEL = 'Japanese(ja_JP)';
 const LOGIN_NOTICE_COPY =
   
'\u767b\u5f55\u6210\u529f\u540e\u4f1a\u81ea\u52a8\u6062\u590d\u5f53\u524d\u5de5\u4f5c\u53f0\u4f1a\u8bdd\uff0c\u5e76\u5728\u9700\u8981\u65f6\u5c1d\u8bd5\u5237\u65b0\u4ee4\u724c\u3002';
-const HERO_TITLE = 'Open-source enterprise observability for private 
operations';
-const HERO_LEAD = 'Collectors, monitoring templates, entities, metrics, logs, 
and traces';
-const HERO_FOCUS = 'Handle alerts and close issues inside HertzBeat';
+const HERO_TITLE = '开源私有化企业运维可观测平台';
+const HERO_LEAD = '采集器、监控模板、实体、指标、日志和链路';
+const HERO_FOCUS = '处理告警并关闭问题';
 const HERO_BODY =
-  'Collect metrics from applications, databases, operating systems, 
middleware, and network devices without sending data outside your deployment.';
-const LOGIN_HEADING = 'Sign In HertzBeat';
-const USERNAME_PROMPT = 'Please enter your username';
-const PASSWORD_PROMPT = 'Please enter password';
-const LOGIN_BUTTON = 'Login';
+  '通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。';
+const LOGIN_HEADING = '登入 HertzBeat';
+const USERNAME_PROMPT = '请输入用户名';
+const PASSWORD_PROMPT = '请输入密码';
+const PASSWORD_TOGGLE_LABEL = '显示密码';
+const REMEMBER_ME_LABEL = '记住我';
+const LOGIN_BUTTON = '登录';
 
 vi.mock('next/navigation', () => ({
   useRouter: () => ({
     replace: vi.fn()
-  }),
-  useSearchParams: () => ({
-    get: () => null
   })
 }));
 
 vi.mock('next/image', () => ({
-  default: ({ alt, src, priority: _priority, ...props }: any) => <img 
alt={alt} src={src} {...props} />
+  default: ({ alt, src, priority: _priority, ...props }: any) => 
React.createElement('img', { alt, src, ...props })
 }));
 
 vi.mock('../providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
+    t: createTranslatorMock({ locale: 'zh-CN' }),
     locale: 'zh-CN',
     locales: [
       { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 
'🇬🇧' },
@@ -55,10 +54,9 @@ vi.mock('../ui/input', () => ({
 }));
 
 vi.mock('../../lib/passport-login/controller', () => ({
-  assertLoginSuccess: vi.fn(),
+  assertSessionLoginSuccess: vi.fn(),
   buildLoginRequestBody: vi.fn(),
   bootstrapPostLoginSession: vi.fn(),
-  persistLoginTokens: vi.fn(),
   resolvePostLoginRedirectTarget: vi.fn(() => '/overview'),
   LOGIN_REDIRECT_QUERY_KEY: 'redirect'
 }));
@@ -86,7 +84,7 @@ describe('LoginForm', () => {
     expect(html).toContain(HERO_LEAD);
     expect(html).toContain(HERO_FOCUS);
     expect(html).toContain(HERO_BODY);
-    expect(html).not.toContain('Open-source private-deployable enterprise 
operations observability platform');
+    expect(html).not.toContain('Open-source enterprise observability for 
private operations');
     expect(html).not.toContain('Unified metrics platform, agentless and 
supports web, db, os, mid, network etc.');
     expect(html).not.toContain('Unified logs platform');
     expect(html).not.toContain('seamlessly integrates');
@@ -95,11 +93,35 @@ describe('LoginForm', () => {
     expect(html).toContain(USERNAME_PROMPT);
     expect(html).toContain(PASSWORD_PROMPT);
     expect(html).toContain(LOGIN_BUTTON);
-    expect(html).toContain('placeholder="Please enter your username"');
-    expect(html).toContain('placeholder="Please enter password"');
+    expect(html).toContain(`placeholder="${USERNAME_PROMPT}"`);
+    expect(html).toContain(`placeholder="${PASSWORD_PROMPT}"`);
+    expect(html).toContain(`aria-label="${PASSWORD_TOGGLE_LABEL}"`);
     expect(html).toContain('data-passport-login-password-eye="true"');
     expect(html).toContain('data-passport-login-panel="angular-gray-card"');
     expect(html).toContain('data-passport-login-panel-align="angular-top"');
+    
expect(html).toContain('data-passport-login-submit-lifecycle-contract="angular-required-default-warning-session-bootstrap-redirect"');
+    
expect(html).toContain('data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"');
+    
expect(html).toContain('data-passport-login-required-mode-contract="angular-required-no-trim"');
+    
expect(html).toContain('data-passport-login-required-mode-owner="hertzbeat-ui-passport-login-action"');
+    
expect(html).toContain('data-passport-login-session-user-name-contract="angular-raw-identifier"');
+    
expect(html).toContain('data-passport-login-session-user-name-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-hz-ui="passport-login-action-frame"');
+    
expect(html).toContain('data-hz-passport-login-action-owner="hertzbeat-ui-passport-login-action"');
+    
expect(html).toContain('data-hz-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"');
+    
expect(html).toContain('data-hz-passport-login-required-fields="identifier-credential"');
+    
expect(html).toContain('data-hz-passport-login-required-mode="angular-required-no-trim"');
+    
expect(html).toContain('data-hz-passport-login-default-password="angular-first-submit-warning"');
+    
expect(html).toContain('data-hz-passport-login-default-password-lifecycle="angular-sticky-until-submit"');
+    
expect(html).toContain('data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"');
+    
expect(html).toContain('data-hz-passport-login-token-boundary="bff-cookie-no-localstorage"');
+    
expect(html).toContain('data-hz-passport-login-session-bootstrap="angular-startup-load-after-success"');
+    
expect(html).toContain('data-hz-passport-login-session-user-name="angular-raw-identifier"');
+    
expect(html).toContain('data-hz-passport-login-startup-failure="angular-exception-500"');
+    
expect(html).toContain('data-passport-login-startup-failure-contract="angular-exception-500"');
+    
expect(html).toContain('data-hz-passport-login-redirect="angular-referrer-non-passport-fallback"');
+    
expect(html).toContain('data-hz-passport-login-redirect-fallback="angular-root-fallback"');
+    
expect(html).toContain('data-passport-login-redirect-fallback-contract="angular-root-fallback"');
+    expect(html).toContain('data-hz-passport-login-remember-default="true"');
     expect(html).toContain('data-passport-login-accent="true"');
     expect(html).toContain('data-passport-login-remember="true"');
     
expect(html).toContain('data-passport-login-remember-checkbox="cold-checkbox"');
@@ -113,21 +135,23 @@ describe('LoginForm', () => {
     
expect(html).toContain('data-passport-hero-offset="angular-left-reference"');
     expect(html).toContain('data-passport-brand-lockup="angular-lowered"');
     expect(html).toContain('data-passport-intro-bullet-tone="angular-cyan"');
-    expect(html).toContain('Remember me');
+    expect(html).toContain(REMEMBER_ME_LABEL);
     expect(html).toContain('data-passport-locale-trigger="globe"');
     expect(html).toContain('data-passport-footer-tone="angular-muted"');
     expect(html).toContain('data-passport-footer-band="angular-raised"');
     expect(html).not.toContain(ZH_CN_LABEL);
     expect(html).toContain('Apache HertzBeat™');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
-    expect(html).toContain('Licensed under the Apache License, Version 2.0');
+    expect(html).toContain('Copyright ©');
+    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
     expect(html).not.toContain(LOGIN_NOTICE_COPY);
     expect(html).not.toContain('value="admin"');
     expect(html).not.toContain('value="hertzbeat"');
-  }, 15000);
+  }, 60000);
 
   it('removes the remaining bright auth-field residue and adopts shared ops 
form tokens', () => {
     const source = readFileSync(resolve(process.cwd(), 
'components/pages/login-form.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 
'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
 
     expect(source).not.toContain('text-[#424955]');
     expect(source).not.toContain('text-[#697180]');
@@ -142,17 +166,27 @@ describe('LoginForm', () => {
     expect(source).not.toContain('text-[#8f1d37]');
     expect(source).not.toContain('pt-8 lg:pt-7');
 
-    expect(source).toContain('text-[var(--ops-text-secondary)]');
+    expect(uiSource).toContain('text-[var(--ops-text-secondary)]');
     expect(source).toContain('text-[var(--ops-text-tertiary)]');
     expect(source).toContain('border-[var(--ops-border-color)]');
     expect(source).toContain('bg-[var(--ops-surface-panel)]');
-    expect(source).toContain('bg-[var(--ops-surface-elevated)]');
-    expect(source).toContain("from '../workbench/primitives'");
+    expect(uiSource).toContain('bg-[var(--ops-surface-elevated)]');
+    expect(source).toContain("import { HzPassportLoginActionFrame, 
HzPassportLoginNotice, HzPassportLoginValidationNotice } from 
'@hertzbeat/ui';");
+    expect(source).toContain('<HzPassportLoginActionFrame');
     expect(source).toContain("import { Checkbox } from '../ui/checkbox';");
-    expect(source).toContain('StatusState');
+    expect(source).toContain('<HzPassportLoginNotice copy={notice.copy} 
href={notice.href} />');
+    expect(source).toContain('<HzPassportLoginValidationNotice 
title={t(\'common.attention\')} copy={error} />');
+    expect(source).toContain('validateCredentialLoginDraft(identifier, 
credential, t)');
+    expect(source).toContain("import { resetWorkbenchLoadCache } from 
'../../lib/workbench-load-cache';");
+    expect(source).toContain('resetWorkbenchLoadCache();');
+    expect(source).toContain('await bootstrapPostLoginSession(apiGet);');
     expect(source).toContain('data-passport-login-panel-align="angular-top"');
+    expect(source).not.toContain('<AlertTriangle');
     expect(source).not.toContain('input type="checkbox"');
     expect(source).not.toContain('<input type="checkbox" className="sr-only" 
defaultChecked />');
+    expect(source).not.toContain('StatusState');
     expect(source).not.toContain('ObservabilityStatusState');
+    expect(source).toContain("aria-label={showCredential ? 
t('app.login.password-hide') : t('app.login.password-show')}");
+    expect(source).toContain("label={t('app.login.remember-me')}");
   });
 });
diff --git a/web-next/components/pages/login-form.tsx 
b/web-next/components/pages/login-form.tsx
index b8050f8d1c..60b1655582 100644
--- a/web-next/components/pages/login-form.tsx
+++ b/web-next/components/pages/login-form.tsx
@@ -2,23 +2,37 @@
 
 import React from 'react';
 import { FormEvent, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { AlertTriangle, Eye, EyeOff, LockKeyhole, UserRound } from 
'lucide-react';
+import { useRouter } from 'next/navigation';
+import { Eye, EyeOff, LockKeyhole, UserRound } from 'lucide-react';
+import { HzPassportLoginActionFrame, HzPassportLoginNotice, 
HzPassportLoginValidationNotice } from '@hertzbeat/ui';
 import { useI18n } from '../providers/i18n-provider';
 import { Button } from '../ui/button';
 import { Checkbox } from '../ui/checkbox';
 import { Input } from '../ui/input';
-import { StatusState } from '../workbench/primitives';
 import { apiGet } from '../../lib/api-client';
-import { assertLoginSuccess, bootstrapPostLoginSession, buildLoginRequestBody, 
LOGIN_REDIRECT_QUERY_KEY, persistLoginTokens, resolvePostLoginRedirectTarget } 
from '../../lib/passport-login/controller';
-import type { LoginMessage } from '../../lib/passport-login/controller';
-import { buildLoginNotice, shouldBlockDefaultPasswordSubmit, 
shouldWarnDefaultPassword } from '../../lib/passport-login/view-model';
+import {
+  assertSessionLoginSuccess,
+  bootstrapPostLoginSession,
+  buildLoginRequestBody,
+  buildPostLoginSessionUser,
+  resolvePostLoginStartupFailureTarget,
+  type LoginMessage,
+  type PassportLoginRouteState
+} from '../../lib/passport-login/controller';
+import { buildLoginNotice, shouldBlockDefaultPasswordSubmit, 
shouldWarnDefaultPassword, validateCredentialLoginDraft } from 
'../../lib/passport-login/view-model';
+import { writeClientSessionUserSnapshot } from '../../lib/session-client';
+import { markAboutAutoShowAfterLogin } from '../../lib/shell/about';
+import { resetWorkbenchLoadCache } from '../../lib/workbench-load-cache';
 import { PassportPanel, PassportShell } from './passport-shell';
 
-export function LoginForm() {
+const EMPTY_PASSPORT_LOGIN_ROUTE_STATE: PassportLoginRouteState = {
+  redirectTarget: '/'
+};
+
+export function LoginForm({ initialRouteState }: { initialRouteState?: 
PassportLoginRouteState } = {}) {
   const { t } = useI18n();
   const router = useRouter();
-  const searchParams = useSearchParams();
+  const passportLoginRouteState = initialRouteState ?? 
EMPTY_PASSPORT_LOGIN_ROUTE_STATE;
   const [identifier, setIdentifier] = useState('');
   const [credential, setCredential] = useState('');
   const [showCredential, setShowCredential] = useState(false);
@@ -32,13 +46,21 @@ export function LoginForm() {
 
   async function handleSubmit(event: FormEvent) {
     event.preventDefault();
+    const validation = validateCredentialLoginDraft(identifier, credential, t);
+    if (validation) {
+      setError(validation.message);
+      return;
+    }
+
     if (shouldBlockDefaultPasswordSubmit(needUpdatePassword, credential)) {
       setNeedUpdatePassword(true);
       setError(null);
       return;
     }
 
-    setNeedUpdatePassword(shouldWarnDefaultPassword(credential));
+    if (shouldWarnDefaultPassword(credential)) {
+      setNeedUpdatePassword(true);
+    }
     setLoading(true);
     setError(null);
     try {
@@ -48,14 +70,21 @@ export function LoginForm() {
         body: JSON.stringify(buildLoginRequestBody(identifier, credential))
       });
       const message = (await response.json()) as LoginMessage;
-      const tokens = assertLoginSuccess(
+      assertSessionLoginSuccess(
         response.status,
         message,
         t('passport.login.error.with-status')
       );
-      persistLoginTokens(window.localStorage, tokens);
-      await bootstrapPostLoginSession(apiGet);
-      
router.replace(resolvePostLoginRedirectTarget(searchParams.get(LOGIN_REDIRECT_QUERY_KEY)));
+      resetWorkbenchLoadCache();
+      writeClientSessionUserSnapshot(buildPostLoginSessionUser(identifier, 
message));
+      try {
+        await bootstrapPostLoginSession(apiGet);
+      } catch {
+        router.replace(resolvePostLoginStartupFailureTarget());
+        return;
+      }
+      markAboutAutoShowAfterLogin();
+      router.replace(passportLoginRouteState.redirectTarget);
     } catch (err) {
       setError(err instanceof Error ? err.message : 
t('passport.login.error.generic'));
     } finally {
@@ -68,6 +97,11 @@ export function LoginForm() {
       <div
         data-passport-login-panel="angular-gray-card"
         data-passport-login-panel-align="angular-top"
+        
data-passport-login-submit-lifecycle-contract="angular-required-default-warning-session-bootstrap-redirect"
+        
data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"
+        data-passport-login-startup-failure-contract="angular-exception-500"
+        data-passport-login-redirect-fallback-contract="angular-root-fallback"
+        
data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"
       >
         <PassportPanel
           className="rounded-none border border-[rgba(33,35,42,0.68)] 
border-b-[var(--ops-primary)] border-r-[var(--ops-primary)] bg-[#5f5f66] px-5 
py-9 shadow-none"
@@ -83,81 +117,88 @@ export function LoginForm() {
             </div>
           )}
         >
-        <form onSubmit={handleSubmit} className="mt-4 space-y-4">
-          <label className="block space-y-2">
-            <span className={fieldLabelClassName}>
-              {t('app.login.message-need-identifier')}
-            </span>
-            <div className="relative">
-              <UserRound size={16} className={fieldIconClassName} />
-              <Input
-                value={identifier}
-                onChange={e => setIdentifier(e.target.value)}
-                placeholder={t('app.login.message-need-identifier')}
-                className={fieldInputClassName}
-              />
-            </div>
-          </label>
-          <label className="block space-y-2">
-            <span className={fieldLabelClassName}>
-              {t('app.login.message-need-credential')}
-            </span>
-            <div className="relative">
-              <LockKeyhole size={16} className={fieldIconClassName} />
-              <Input
-                value={credential}
-                onChange={e => {
-                  const nextValue = e.target.value;
-                  setCredential(nextValue);
-                  if (!shouldWarnDefaultPassword(nextValue) && 
needUpdatePassword) {
-                    setNeedUpdatePassword(false);
-                  }
-                }}
-                placeholder={t('app.login.message-need-credential')}
-                type={showCredential ? 'text' : 'password'}
-                className={`${fieldInputClassName} pr-10`}
+        <HzPassportLoginActionFrame
+          className="mt-4"
+          
data-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"
+          
data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"
+          data-passport-login-required-mode-contract="angular-required-no-trim"
+          
data-passport-login-required-mode-owner="hertzbeat-ui-passport-login-action"
+          
data-passport-login-session-user-name-contract="angular-raw-identifier"
+          
data-passport-login-session-user-name-owner="hertzbeat-ui-passport-login-action"
+        >
+          <form onSubmit={handleSubmit} className="space-y-4">
+            <label className="block space-y-2">
+              <span className={fieldLabelClassName}>
+                {t('app.login.message-need-identifier')}
+              </span>
+              <div className="relative">
+                <UserRound size={16} className={fieldIconClassName} />
+                <Input
+                  value={identifier}
+                  onChange={e => {
+                    setIdentifier(e.target.value);
+                    if (error) {
+                      setError(null);
+                    }
+                  }}
+                  placeholder={t('app.login.message-need-identifier')}
+                  className={fieldInputClassName}
+                />
+              </div>
+            </label>
+            <label className="block space-y-2">
+              <span className={fieldLabelClassName}>
+                {t('app.login.message-need-credential')}
+              </span>
+              <div className="relative">
+                <LockKeyhole size={16} className={fieldIconClassName} />
+                <Input
+                  value={credential}
+                  onChange={e => {
+                    const nextValue = e.target.value;
+                    setCredential(nextValue);
+                    if (error) {
+                      setError(null);
+                    }
+                  }}
+                  placeholder={t('app.login.message-need-credential')}
+                  type={showCredential ? 'text' : 'password'}
+                  className={`${fieldInputClassName} pr-10`}
+                />
+                <button
+                  aria-label={showCredential ? t('app.login.password-hide') : 
t('app.login.password-show')}
+                  className="absolute right-3 top-1/2 inline-flex 
-translate-y-1/2 items-center justify-center text-[var(--ops-text-tertiary)] 
transition-colors hover:text-white"
+                  data-passport-login-password-eye="true"
+                  type="button"
+                  onClick={() => setShowCredential(current => !current)}
+                >
+                  {showCredential ? <EyeOff size={16} aria-hidden="true" /> : 
<Eye size={16} aria-hidden="true" />}
+                </button>
+              </div>
+            </label>
+
+            <div data-passport-login-remember="true">
+              <Checkbox
+                data-passport-login-remember-checkbox="cold-checkbox"
+                defaultChecked
+                containerClassName="min-h-4 gap-2 text-[13px] font-medium 
text-[#17181c]"
+                label={t('app.login.remember-me')}
               />
-              <button
-                aria-label={showCredential ? 'Hide password' : 'Show password'}
-                className="absolute right-3 top-1/2 inline-flex 
-translate-y-1/2 items-center justify-center text-[var(--ops-text-tertiary)] 
transition-colors hover:text-white"
-                data-passport-login-password-eye="true"
-                type="button"
-                onClick={() => setShowCredential(current => !current)}
-              >
-                {showCredential ? <EyeOff size={16} aria-hidden="true" /> : 
<Eye size={16} aria-hidden="true" />}
-              </button>
             </div>
-          </label>
-
-          <div data-passport-login-remember="true">
-            <Checkbox
-              data-passport-login-remember-checkbox="cold-checkbox"
-              defaultChecked
-              containerClassName="min-h-4 gap-2 text-[13px] font-medium 
text-[#17181c]"
-              label="Remember me"
-            />
-          </div>
 
-          {notice.kind === 'warning' ? (
-            <a
-              className="flex items-start gap-3 rounded-[6px] border 
border-[rgba(216,111,91,0.28)] bg-[var(--ops-surface-elevated)] px-4 py-3 
text-sm text-[var(--ops-text-secondary)] transition-colors 
hover:border-[var(--ops-primary)] hover:text-[var(--ops-text-primary)]"
-              href={notice.href}
-              target="_blank"
-              rel="noreferrer"
-            >
-              <AlertTriangle size={16} className="mt-0.5 shrink-0 
text-[var(--ops-primary)]" />
-              <span>{notice.copy}</span>
-            </a>
-          ) : null}
+            {notice.kind === 'warning' ? (
+              <HzPassportLoginNotice copy={notice.copy} href={notice.href} />
+            ) : null}
 
-          <Button className="h-11 w-full" variant="primary" type="submit" 
disabled={loading} size="lg">
-            {loading ? t('passport.login.loading') : t('app.login.login')}
-          </Button>
-        </form>
+            <Button className="h-11 w-full" variant="primary" type="submit" 
disabled={loading} size="lg">
+              {loading ? t('passport.login.loading') : t('app.login.login')}
+            </Button>
+          </form>
+        </HzPassportLoginActionFrame>
 
         {error ? (
           <div className="mt-4">
-            <StatusState title={t('common.attention')} copy={error} 
tone="danger" />
+            <HzPassportLoginValidationNotice title={t('common.attention')} 
copy={error} />
           </div>
         ) : null}
         </PassportPanel>
diff --git a/web-next/components/pages/passport-shell-session-clear.test.tsx 
b/web-next/components/pages/passport-shell-session-clear.test.tsx
new file mode 100644
index 0000000000..78394261fd
--- /dev/null
+++ b/web-next/components/pages/passport-shell-session-clear.test.tsx
@@ -0,0 +1,125 @@
+// @vitest-environment jsdom
+
+import React from 'react';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { HB_UI_SESSION_USER_KEY } from '../../lib/session-client';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean 
}).IS_REACT_ACT_ENVIRONMENT = true;
+
+vi.mock('next/image', () => ({
+  default: ({ alt, src, priority: _priority, ...props }: any) => 
React.createElement('img', { alt, src, ...props })
+}));
+
+vi.mock('../providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({ locale: 'zh-CN' }),
+    locale: 'zh-CN',
+    locales: [
+      { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 
'gb' },
+      { code: 'zh-CN', labelKey: 'settings.system-config.locale.zh_CN', abbr: 
'cn' }
+    ],
+    setLocale: vi.fn()
+  })
+}));
+
+vi.mock('../shell/locale-option-list', () => ({
+  LocaleOptionList: () => <div data-locale-options="true">locale-options</div>
+}));
+
+vi.mock('../shell/platform-copyright-footer', () => ({
+  PlatformCopyrightFooter: ({
+    children,
+    headlineClassName: _headlineClassName,
+    innerClassName: _innerClassName,
+    lineClassName: _lineClassName,
+    linkClassName: _linkClassName,
+    version: _version,
+    ...props
+  }: any) => <footer {...props}>{children}</footer>
+}));
+
+describe('PassportShell session clear lifecycle', () => {
+  let container: HTMLDivElement;
+  let root: Root;
+
+  async function mountShell() {
+    const { PassportShell } = await import('./passport-shell');
+    await act(async () => {
+      root.render(
+        <PassportShell>
+          <div data-passport-child="true">child</div>
+        </PassportShell>
+      );
+      await Promise.resolve();
+    });
+  }
+
+  async function mountLockShell() {
+    const { PassportShell } = await import('./passport-shell');
+    await act(async () => {
+      root.render(
+        <PassportShell sessionLifecycle="preserve-on-lock">
+          <div data-passport-child="true">child</div>
+        </PassportShell>
+      );
+      await Promise.resolve();
+    });
+  }
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+    window.sessionStorage.clear();
+    document.cookie = 'hb_ui_session=1; path=/';
+    vi.stubGlobal('fetch', vi.fn());
+  });
+
+  afterEach(() => {
+    act(() => {
+      root.unmount();
+    });
+    container.remove();
+    window.sessionStorage.clear();
+    vi.unstubAllGlobals();
+  });
+
+  it('clears Angular-style client session residue on passport entry without 
calling the logout API', async () => {
+    window.sessionStorage.setItem(
+      HB_UI_SESSION_USER_KEY,
+      JSON.stringify({ name: 'admin', avatar: './assets/img/avatar.svg', 
email: 'administrator', role: 'ADMIN' })
+    );
+
+    await mountShell();
+
+    
expect(container.querySelector('[data-hz-ui="passport-session-clear-frame"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-session-clear-contract="angular-token-service-clear-on-passport-entry"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-session-clear-lifecycle="angular-token-service-clear-on-passport-entry"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-session-clear-scope="client-marker-user-snapshot"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-session-clear-boundary="no-api-logout-on-entry"]')).not.toBeNull();
+    expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).toBeNull();
+    expect(document.cookie).not.toContain('hb_ui_session=1');
+    expect(global.fetch).not.toHaveBeenCalled();
+  }, 15000);
+
+  it('preserves client session residue on the Angular lock entry', async () => 
{
+    window.sessionStorage.setItem(
+      HB_UI_SESSION_USER_KEY,
+      JSON.stringify({ name: 'admin', avatar: './assets/img/avatar.svg', 
email: 'administrator', role: 'ADMIN' })
+    );
+
+    await mountLockShell();
+
+    
expect(container.querySelector('[data-passport-session-clear-contract="angular-lock-preserve-session"]')).not.toBeNull();
+    
expect(container.querySelector('[data-passport-session-clear-enabled="false"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"]')).not.toBeNull();
+    
expect(container.querySelector('[data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"]')).not.toBeNull();
+    
expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).toContain('"name":"admin"');
+    expect(document.cookie).toContain('hb_ui_session=1');
+    expect(global.fetch).not.toHaveBeenCalled();
+  }, 15000);
+});
diff --git a/web-next/components/pages/passport-shell.test.tsx 
b/web-next/components/pages/passport-shell.test.tsx
index 42cb958e61..0ef944d5f0 100644
--- a/web-next/components/pages/passport-shell.test.tsx
+++ b/web-next/components/pages/passport-shell.test.tsx
@@ -7,12 +7,12 @@ import { PassportPanel, PassportShell } from 
'./passport-shell';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 vi.mock('next/image', () => ({
-  default: ({ alt, src, priority: _priority, ...props }: any) => <img 
alt={alt} src={src} {...props} />
+  default: ({ alt, src, priority: _priority, ...props }: any) => 
React.createElement('img', { alt, src, ...props })
 }));
 
 vi.mock('../providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
+    t: createTranslatorMock({ locale: 'zh-CN' }),
     locale: 'zh-CN',
     locales: [
       { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 
'🇬🇧' },
@@ -39,7 +39,7 @@ vi.mock('../shell/platform-copyright-footer', () => ({
     <footer {...props}>
       <div className={headlineClassName}>Apache HertzBeat™{version ? ` 
${version}` : ''}</div>
       <a className={linkClassName}>Apache HertzBeat™</a>
-      <div className={lineClassName}>{children ?? 'Licensed under the Apache 
License, Version 2.0'}</div>
+      {children ? <div className={lineClassName}>{children}</div> : null}
     </footer>
   )
 }));
@@ -57,6 +57,13 @@ describe('PassportShell', () => {
     );
 
     expect(html).toContain('data-passport-shell="true"');
+    expect(html).toContain('data-hz-ui="passport-session-clear-frame"');
+    
expect(html).toContain('data-passport-session-clear-contract="angular-token-service-clear-on-passport-entry"');
+    expect(html).toContain('data-passport-session-clear-enabled="true"');
+    
expect(html).toContain('data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"');
+    
expect(html).toContain('data-hz-passport-session-clear-lifecycle="angular-token-service-clear-on-passport-entry"');
+    
expect(html).toContain('data-hz-passport-session-clear-scope="client-marker-user-snapshot"');
+    
expect(html).toContain('data-hz-passport-session-clear-boundary="no-api-logout-on-entry"');
     expect(html).toContain('data-passport-shell-spacing="angular-reference"');
     expect(html).toContain('data-passport-brand-lockup="angular-lowered"');
     
expect(html).toContain('data-passport-content-alignment="angular-centered"');
@@ -76,7 +83,32 @@ describe('PassportShell', () => {
     expect(html).toContain('data-passport-footer-band="angular-raised"');
     expect(html).toContain('/assets/bg.png');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
-    expect(html).toContain('Licensed under the Apache License, Version 2.0');
+    expect(html).toContain('开源私有化企业运维可观测平台');
+    expect(html).toContain('采集器、监控模板、实体、指标、日志和链路');
+    expect(html).toContain('处理告警并关闭问题');
+    expect(html).toContain('通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。');
+    expect(html).toContain('按网络区域扩展 Collector 集群,支撑私有化、隔离采集和状态页。');
+    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
+    expect(html).not.toContain('Apache License, Version 2.0');
+  });
+
+  it('can preserve the active session for the Angular lock route', () => {
+    const html = renderToStaticMarkup(
+      <PassportShell panelClassName="max-w-[712px]" 
sessionLifecycle="preserve-on-lock">
+        <PassportPanel title="Unlock">
+          <form data-passport-lock-form="true">
+            <input />
+          </form>
+        </PassportPanel>
+      </PassportShell>
+    );
+
+    
expect(html).toContain('data-passport-session-clear-contract="angular-lock-preserve-session"');
+    expect(html).toContain('data-passport-session-clear-enabled="false"');
+    
expect(html).toContain('data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"');
+    
expect(html).toContain('data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"');
+    
expect(html).toContain('data-hz-passport-session-clear-boundary="no-session-clear-on-lock"');
+    expect(html).toContain('data-passport-lock-form="true"');
   });
 
   it('removes the remaining bright auth shell residue and adopts ops tokens', 
() => {
@@ -104,9 +136,16 @@ describe('PassportShell', () => {
     expect(source).toContain('text-[var(--ops-text-secondary)]');
     expect(source).toContain('text-[var(--ops-primary)]');
     expect(source).toContain("from '../workbench/primitives'");
+    expect(source).toContain("import { HzPassportSessionClearFrame } from 
'@hertzbeat/ui';");
+    expect(source).toContain("import { clearClientSessionMarker, 
clearClientSessionUserSnapshot } from '../../lib/session-client';");
+    expect(source).toContain('clearClientSessionMarker();');
+    expect(source).toContain('clearClientSessionUserSnapshot();');
+    expect(source).not.toContain('clearClientSession();');
     expect(source).toContain('WorkbenchPanel');
     expect(source).toContain('data-passport-locale-trigger="globe"');
     expect(source).toContain('data-passport-locale-tone="angular-magenta"');
+    expect(source).toContain("aria-label={t('app.passport.language-switch')}");
+    expect(source).not.toContain('aria-label="Switch language"');
     expect(source).toContain('text-[#d11ce6]');
     
expect(source).toContain('data-passport-background-overlay="angular-light"');
     
expect(source).toContain('data-passport-intro-list="angular-single-column"');
diff --git a/web-next/components/pages/passport-shell.tsx 
b/web-next/components/pages/passport-shell.tsx
index 325dc16bd8..80a12be448 100644
--- a/web-next/components/pages/passport-shell.tsx
+++ b/web-next/components/pages/passport-shell.tsx
@@ -1,17 +1,20 @@
 'use client';
 
-import React, { type ReactNode, useState } from 'react';
+import React, { type ReactNode, useEffect, useState } from 'react';
 import Image from 'next/image';
 import { Globe2 } from 'lucide-react';
+import { HzPassportSessionClearFrame } from '@hertzbeat/ui';
 import { useI18n } from '../providers/i18n-provider';
 import { LocaleOptionList } from '../shell/locale-option-list';
 import { PlatformCopyrightFooter } from '../shell/platform-copyright-footer';
 import { WorkbenchPanel } from '../workbench/primitives';
+import { clearClientSessionMarker, clearClientSessionUserSnapshot } from 
'../../lib/session-client';
 import { cn } from '../../lib/utils';
 
 type PassportShellProps = {
   children: ReactNode;
   panelClassName?: string;
+  sessionLifecycle?: 'clear-on-entry' | 'preserve-on-lock';
 };
 
 type PassportPanelProps = {
@@ -21,9 +24,11 @@ type PassportPanelProps = {
   className?: string;
 };
 
-export function PassportShell({ children, panelClassName }: 
PassportShellProps) {
+export function PassportShell({ children, panelClassName, sessionLifecycle = 
'clear-on-entry' }: PassportShellProps) {
   const { t, locale, locales, setLocale } = useI18n();
   const [localeOpen, setLocaleOpen] = useState(false);
+  const shouldClearSession = sessionLifecycle === 'clear-on-entry';
+  const sessionContract = shouldClearSession ? 
'angular-token-service-clear-on-passport-entry' : 
'angular-lock-preserve-session';
   const passportPoints = [
     t('about.point.1'),
     t('about.point.2'),
@@ -33,11 +38,23 @@ export function PassportShell({ children, panelClassName }: 
PassportShellProps)
     t('about.point.6')
   ];
 
+  useEffect(() => {
+    if (!shouldClearSession) {
+      return;
+    }
+    clearClientSessionMarker();
+    clearClientSessionUserSnapshot();
+  }, [shouldClearSession]);
+
   return (
-    <div
+    <HzPassportSessionClearFrame
       className="relative min-h-screen overflow-hidden 
bg-[var(--ops-background)] bg-cover bg-center bg-no-repeat 
text-[var(--ops-text-primary)]"
+      lifecycle={sessionLifecycle}
       data-login-shell="passport"
       data-passport-shell="true"
+      data-passport-session-clear-contract={sessionContract}
+      data-passport-session-clear-enabled={shouldClearSession ? 'true' : 
'false'}
+      data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"
       style={{ backgroundImage: "url('/assets/bg.png')" }}
     >
       <div
@@ -53,7 +70,7 @@ export function PassportShell({ children, panelClassName }: 
PassportShellProps)
             <div className="absolute right-0 top-0 hidden md:block">
               <div className="relative">
                 <button
-                  aria-label="Switch language"
+                  aria-label={t('app.passport.language-switch')}
                   className="inline-flex h-9 w-9 items-center justify-center 
text-[#d11ce6] transition-colors hover:text-[#f149ff]"
                   data-passport-locale-trigger="globe"
                   data-passport-locale-tone="angular-magenta"
@@ -149,7 +166,7 @@ export function PassportShell({ children, panelClassName }: 
PassportShellProps)
           />
         </div>
       </div>
-    </div>
+    </HzPassportSessionClearFrame>
   );
 }
 
diff --git a/web-next/lib/passport-lock/view-model.test.ts 
b/web-next/lib/passport-lock/view-model.test.ts
index a5a34bed59..9b650e5858 100644
--- a/web-next/lib/passport-lock/view-model.test.ts
+++ b/web-next/lib/passport-lock/view-model.test.ts
@@ -8,14 +8,14 @@ describe('passport lock view model', () => {
   it('builds lock page facts', () => {
     expect(buildLockFacts(t)).toEqual([
       { label: '工作区', value: 'passport/lock' },
-      { label: '状态', value: 'interactive' },
-      { label: '下一步', value: 'wire unlock validation if needed' }
+      { label: '状态', value: '可解锁' },
+      { label: '下一步', value: '按需接入解锁校验' }
     ]);
   });
 
   it('requires a non-empty password', () => {
     expect(validateUnlockPassword('', t)).toBe('请输入密码');
-    expect(validateUnlockPassword('   ', t)).toBe('请输入密码');
+    expect(validateUnlockPassword('   ', t)).toBeNull();
     expect(validateUnlockPassword('secret', t)).toBeNull();
   });
 });
diff --git a/web-next/lib/passport-lock/view-model.ts 
b/web-next/lib/passport-lock/view-model.ts
index 72e5114a7d..6d692170da 100644
--- a/web-next/lib/passport-lock/view-model.ts
+++ b/web-next/lib/passport-lock/view-model.ts
@@ -3,13 +3,13 @@ type Translator = (key: string, params?: Record<string, 
string | number | null |
 export function buildLockFacts(t: Translator) {
   return [
     { label: t('common.workspace'), value: 'passport/lock' },
-    { label: t('common.status'), value: 'interactive' },
+    { label: t('common.status'), value: t('passport.lock.status.interactive') 
},
     { label: t('common.next-step'), value: t('passport.lock.next-step') }
   ];
 }
 
 export function validateUnlockPassword(password: string, t: Translator) {
-  if (!password.trim()) {
+  if (password.length === 0) {
     return t('passport.lock.error.required');
   }
   return null;
diff --git a/web-next/lib/passport-login/controller.test.ts 
b/web-next/lib/passport-login/controller.test.ts
index 73759e5a7a..5d04bc41e3 100644
--- a/web-next/lib/passport-login/controller.test.ts
+++ b/web-next/lib/passport-login/controller.test.ts
@@ -1,13 +1,17 @@
 import { describe, expect, it, vi } from 'vitest';
 import {
   assertLoginSuccess,
+  assertSessionLoginSuccess,
   bootstrapPostLoginSession,
+  buildLoginCompatRouteUrl,
   buildLoginRedirectHref,
   buildLoginRequestBody,
   buildLoginReturnTo,
-  persistLoginTokens,
+  buildPostLoginSessionUser,
+  readPassportLoginRouteState,
   resolveLoginError,
   resolvePostLoginRedirectTarget,
+  resolvePostLoginStartupFailureTarget,
   sanitizeLoginRedirectTarget
 } from './controller';
 
@@ -36,13 +40,43 @@ describe('passport login controller', () => {
     expect(() => assertLoginSuccess(200, { code: 0, data: { token: 'access' } 
}, 'Login failed: {{status}}')).toThrow('Login failed: 200');
   });
 
-  it('persists access and refresh tokens', () => {
-    const storage = { setItem: vi.fn() };
+  it('accepts BFF-cookie login success without exposing tokens to the 
browser', () => {
+    expect(
+      assertSessionLoginSuccess(200, { code: 0, data: { authenticated: true } 
}, 'Login failed: {{status}}')
+    ).toBeUndefined();
+
+    expect(() => assertSessionLoginSuccess(401, { code: 1 }, 'Login failed: 
{{status}}')).toThrow('Login failed: 401');
+    expect(() => assertSessionLoginSuccess(200, { code: 1, msg: 'denied' }, 
'Login failed: {{status}}')).toThrow('denied');
+  });
+
+  it('builds the Angular-style post-login user snapshot from the raw submitted 
identifier', () => {
+    expect(
+      buildPostLoginSessionUser(' ops-admin ', {
+        code: 0,
+        data: {
+          authenticated: true,
+          tokenBoundary: 'bff-cookie',
+          role: 'ADMIN'
+        }
+      })
+    ).toEqual({
+      name: ' ops-admin ',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator',
+      role: 'ADMIN'
+    });
 
-    persistLoginTokens(storage, { token: 'access', refreshToken: 'refresh' });
+    expect(buildPostLoginSessionUser('   ', { code: 0, data: { authenticated: 
true } })).toEqual({
+      name: '   ',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator'
+    });
 
-    expect(storage.setItem).toHaveBeenNthCalledWith(1, 'Authorization', 
'access');
-    expect(storage.setItem).toHaveBeenNthCalledWith(2, 'refresh-token', 
'refresh');
+    expect(buildPostLoginSessionUser('', { code: 0, data: { authenticated: 
true } })).toEqual({
+      name: 'admin',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator'
+    });
   });
 
   it('sanitizes login redirect targets to internal non-auth routes', () => {
@@ -87,20 +121,68 @@ describe('passport login controller', () => {
     expect(buildLoginRedirectHref('/passport/login')).toBe('/passport/login');
     expect(buildLoginRedirectHref('/passport/login', '/login')).toBe('/login');
     
expect(resolvePostLoginRedirectTarget('/monitors?app=website')).toBe('/monitors?app=website');
-    
expect(resolvePostLoginRedirectTarget('/passport/login')).toBe('/overview');
-    
expect(resolvePostLoginRedirectTarget('https://example.com')).toBe('/overview');
+    expect(resolvePostLoginRedirectTarget('/passport/login')).toBe('/');
+    expect(resolvePostLoginRedirectTarget('https://example.com')).toBe('/');
+    expect(resolvePostLoginRedirectTarget('/passport/login', 
'/overview')).toBe('/overview');
+    expect(resolvePostLoginStartupFailureTarget()).toBe('/exception/500');
+  });
+
+  it('builds the compatibility login alias URL while preserving guard 
context', () => {
+    expect(buildLoginCompatRouteUrl()).toBe('/passport/login');
+    expect(buildLoginCompatRouteUrl({ redirect: '/monitors?app=website', 
source: 'guard' })).toBe(
+      '/passport/login?redirect=%2Fmonitors%3Fapp%3Dwebsite&source=guard'
+    );
+    expect(buildLoginCompatRouteUrl({ redirect: '/trace/manage?traceId=1', 
returnLabel: 'Login' })).toBe(
+      '/passport/login?redirect=%2Ftrace%2Fmanage%3FtraceId%3D1'
+    );
+  });
+
+  it('normalizes multi-value URL search params into the first passport login 
redirect target', () => {
+    expect(
+      readPassportLoginRouteState({
+        redirect: ['/monitors?app=website', '/trace/manage?traceId=1'],
+        returnLabel: ['Login', 'Ignored']
+      })
+    ).toEqual({
+      redirectTarget: '/monitors?app=website'
+    });
+
+    expect(
+      readPassportLoginRouteState({
+        redirect: ['https://evil.example', '/monitors?app=website']
+      })
+    ).toEqual({
+      redirectTarget: '/'
+    });
   });
 
-  it('warms the post-login session bootstrap without failing on bootstrap 
misses', async () => {
+  it('warms the post-login session bootstrap after login success', async () => 
{
     const apiGet = vi
       .fn()
-      .mockResolvedValueOnce({ locale: 'en-US' });
+      .mockResolvedValueOnce({ code: 0, data: { locale: 'zh_CN' } })
+      .mockResolvedValueOnce({ code: 0, data: [] });
 
     await expect(bootstrapPostLoginSession(apiGet as 
any)).resolves.toBeUndefined();
-    expect(apiGet).toHaveBeenCalledWith('/config/system');
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/config/system');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/apps/hierarchy?lang=zh-CN');
 
-    const failingApiGet = vi.fn().mockRejectedValueOnce(new Error('bootstrap 
failed'));
+    const failingApiGet = vi
+      .fn()
+      .mockRejectedValueOnce(new Error('bootstrap failed'))
+      .mockResolvedValueOnce({ code: 0, data: [] });
     await expect(bootstrapPostLoginSession(failingApiGet as 
any)).resolves.toBeUndefined();
-    expect(failingApiGet).toHaveBeenCalledWith('/config/system');
+    expect(failingApiGet).toHaveBeenNthCalledWith(1, '/config/system');
+    expect(failingApiGet).toHaveBeenNthCalledWith(2, 
'/apps/hierarchy?lang=en-US');
+  });
+
+  it('preserves Angular startup failure routing when the hierarchy bootstrap 
fails', async () => {
+    const apiGet = vi
+      .fn()
+      .mockResolvedValueOnce({ code: 0, data: { locale: 'zh_CN' } })
+      .mockRejectedValueOnce(new Error('hierarchy failed'));
+
+    await expect(bootstrapPostLoginSession(apiGet as 
any)).rejects.toThrow('hierarchy failed');
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/config/system');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/apps/hierarchy?lang=zh-CN');
   });
 });
diff --git a/web-next/lib/passport-login/controller.ts 
b/web-next/lib/passport-login/controller.ts
index 6eca563773..d9682b901c 100644
--- a/web-next/lib/passport-login/controller.ts
+++ b/web-next/lib/passport-login/controller.ts
@@ -1,21 +1,42 @@
+import { buildCompatRedirectTarget, createCompatSearchParamReader, type 
SearchParamsRecord } from '../compat/search-params';
 import { stripReturnLabelFromHref } from '../signal-route-context';
 
+export type { SearchParamsRecord } from '../compat/search-params';
+
 export type LoginTokens = {
   token: string;
   refreshToken: string;
 };
 
+export type PostLoginSessionUser = {
+  name: string;
+  avatar: string;
+  email: string;
+  role?: string;
+};
+
+export type PassportLoginSearchParams = SearchParamsRecord;
+
+export type PassportLoginRouteState = {
+  redirectTarget: string;
+};
+
 export const DEFAULT_LOGIN_ENTRY_PATH = '/passport/login';
 export const LOGIN_REDIRECT_QUERY_KEY = 'redirect';
+export const POST_LOGIN_STARTUP_FAILURE_PATH = '/exception/500';
+
+export function buildLoginCompatRouteUrl(searchParams?: SearchParamsRecord) {
+  return buildCompatRedirectTarget(DEFAULT_LOGIN_ENTRY_PATH, searchParams);
+}
 
 export type LoginMessage = {
   code?: number;
   msg?: string;
-  data?: Partial<LoginTokens>;
-};
-
-type StorageLike = {
-  setItem: (key: string, value: string) => void;
+  data?: Partial<LoginTokens> & {
+    authenticated?: boolean;
+    tokenBoundary?: string;
+    role?: unknown;
+  };
 };
 
 type LocationLike = {
@@ -26,6 +47,13 @@ type LocationLike = {
 
 type ApiGetter = <T>(path: string) => Promise<T>;
 
+type SystemConfigPayload = {
+  code?: number;
+  data?: {
+    locale?: unknown;
+  } | null;
+};
+
 export function buildLoginRequestBody(identifier: string, credential: string) {
   return {
     type: 0,
@@ -49,9 +77,24 @@ export function assertLoginSuccess(status: number, message: 
LoginMessage, fallba
   };
 }
 
-export function persistLoginTokens(storage: StorageLike, tokens: LoginTokens) {
-  storage.setItem('Authorization', tokens.token);
-  storage.setItem('refresh-token', tokens.refreshToken);
+export function assertSessionLoginSuccess(status: number, message: 
LoginMessage, fallback: string) {
+  if (status >= 400 || message.code !== 0) {
+    throw new Error(resolveLoginError(status, message, fallback));
+  }
+}
+
+export function buildPostLoginSessionUser(identifier: string, message: 
LoginMessage): PostLoginSessionUser {
+  const name = identifier || 'admin';
+  const role = typeof message.data?.role === 'string' && 
message.data.role.trim()
+    ? message.data.role.trim()
+    : undefined;
+
+  return {
+    name,
+    avatar: './assets/img/avatar.svg',
+    email: 'administrator',
+    ...(role ? { role } : {})
+  };
 }
 
 export function sanitizeLoginRedirectTarget(value?: string | null) {
@@ -91,12 +134,35 @@ export function buildLoginRedirectHref(returnTo?: string | 
null, loginPath?: str
   return `${resolvedLoginPath}?${params.toString()}`;
 }
 
-export function resolvePostLoginRedirectTarget(returnTo?: string | null, 
fallback = '/overview') {
+export function resolvePostLoginRedirectTarget(returnTo?: string | null, 
fallback = '/') {
   return sanitizeLoginRedirectTarget(returnTo) || fallback;
 }
 
-export async function bootstrapPostLoginSession(apiGet: ApiGetter) {
-  await Promise.allSettled([
-    apiGet('/config/system')
-  ]);
+export function resolvePostLoginStartupFailureTarget() {
+  return POST_LOGIN_STARTUP_FAILURE_PATH;
+}
+
+export function readPassportLoginRouteState(searchParams?: 
PassportLoginSearchParams): PassportLoginRouteState {
+  const reader = createCompatSearchParamReader(searchParams);
+  return {
+    redirectTarget: 
resolvePostLoginRedirectTarget(reader.get(LOGIN_REDIRECT_QUERY_KEY))
+  };
+}
+
+function normalizeStartupLocale(locale: unknown, fallback = 'en-US') {
+  if (typeof locale !== 'string') return fallback;
+  const nextLocale = locale.trim().replace('_', '-');
+  return nextLocale || fallback;
+}
+
+export async function bootstrapPostLoginSession(apiGet: ApiGetter, 
fallbackLocale = 'en-US') {
+  let configPayload: SystemConfigPayload | null = null;
+  try {
+    configPayload = await apiGet<SystemConfigPayload>('/config/system');
+  } catch {
+    configPayload = null;
+  }
+  const startupLocale = normalizeStartupLocale(configPayload?.data?.locale, 
fallbackLocale);
+
+  await apiGet(`/apps/hierarchy?lang=${encodeURIComponent(startupLocale)}`);
 }
diff --git a/web-next/lib/passport-login/view-model.test.ts 
b/web-next/lib/passport-login/view-model.test.ts
index de2f1c985b..8a8ee1c445 100644
--- a/web-next/lib/passport-login/view-model.test.ts
+++ b/web-next/lib/passport-login/view-model.test.ts
@@ -1,8 +1,9 @@
 import { describe, expect, it, vi } from 'vitest';
-import { buildLoginFeatureCards, buildLoginNotice, 
shouldBlockDefaultPasswordSubmit, shouldWarnDefaultPassword } from 
'./view-model';
+import { buildLoginFeatureCards, buildLoginNotice, 
shouldBlockDefaultPasswordSubmit, shouldWarnDefaultPassword, 
validateCredentialLoginDraft } from './view-model';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const t = createTranslatorMock({ locale: 'zh-CN' });
+const enT = createTranslatorMock({ locale: 'en-US' });
 
 describe('passport login view model', () => {
   it('detects the default password warning condition', () => {
@@ -16,6 +17,20 @@ describe('passport login view model', () => {
     expect(shouldBlockDefaultPasswordSubmit(false, 'custom')).toBe(false);
   });
 
+  it('validates the Angular credential-login required fields before submit', 
() => {
+    expect(validateCredentialLoginDraft('', 'custom', enT)).toEqual({
+      field: 'identifier',
+      message: 'Please enter your username'
+    });
+    expect(validateCredentialLoginDraft('ops-admin', '', enT)).toEqual({
+      field: 'credential',
+      message: 'Please enter password'
+    });
+    expect(validateCredentialLoginDraft(' ops-admin ', ' custom ', 
enT)).toBeNull();
+    expect(validateCredentialLoginDraft('   ', 'custom', enT)).toBeNull();
+    expect(validateCredentialLoginDraft('ops-admin', '   ', enT)).toBeNull();
+  });
+
   it('builds the login feature cards', () => {
     expect(buildLoginFeatureCards(t)).toEqual([
       { title: '运维入口', copy: '登录后继续查看资源、实体、遥测数据和告警。' },
@@ -27,7 +42,7 @@ describe('passport login view model', () => {
   it('builds the warning or session notice', () => {
     expect(buildLoginNotice(true, t)).toEqual({
       kind: 'warning',
-      copy: '当前使用默认密码,建议登录后尽快修改。',
+      copy: '请及时更新初始默认密码!',
       href: 'https://hertzbeat.apache.org/docs/start/account-modify'
     });
 
@@ -36,4 +51,14 @@ describe('passport login view model', () => {
       copy: '登录成功后会自动恢复当前工作台会话,并在需要时尝试刷新令牌。'
     });
   });
+
+  it('builds the English session notice without localized fallback copy', () 
=> {
+    const notice = buildLoginNotice(false, enT);
+
+    expect(notice).toEqual({
+      kind: 'session',
+      copy: 'After login, HertzBeat will restore the current workspace session 
and try to refresh tokens when needed.'
+    });
+    expect(notice.copy).not.toMatch(/[\u4e00-\u9fff]/);
+  });
 });
diff --git a/web-next/lib/passport-login/view-model.ts 
b/web-next/lib/passport-login/view-model.ts
index 75cfb85b56..821fc01e04 100644
--- a/web-next/lib/passport-login/view-model.ts
+++ b/web-next/lib/passport-login/view-model.ts
@@ -8,6 +8,24 @@ export function 
shouldBlockDefaultPasswordSubmit(needUpdatePassword: boolean, cr
   return !needUpdatePassword && shouldWarnDefaultPassword(credential);
 }
 
+export function validateCredentialLoginDraft(identifier: string, credential: 
string, t: Translator) {
+  if (identifier.length === 0) {
+    return {
+      field: 'identifier' as const,
+      message: t('app.login.message-need-identifier')
+    };
+  }
+
+  if (credential.length === 0) {
+    return {
+      field: 'credential' as const,
+      message: t('app.login.message-need-credential')
+    };
+  }
+
+  return null;
+}
+
 export function buildLoginFeatureCards(t: Translator) {
   return [
     {
@@ -29,7 +47,7 @@ export function buildLoginNotice(needUpdatePassword: boolean, 
t: Translator) {
   if (needUpdatePassword) {
     return {
       kind: 'warning' as const,
-      copy: t('passport.login.default-password-warning'),
+      copy: t('app.login.need-change-password'),
       href: 'https://hertzbeat.apache.org/docs/start/account-modify'
     };
   }
diff --git a/web-next/lib/session-bff.test.ts b/web-next/lib/session-bff.test.ts
new file mode 100644
index 0000000000..634cfc6d3f
--- /dev/null
+++ b/web-next/lib/session-bff.test.ts
@@ -0,0 +1,178 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+  buildBackendApiUrl,
+  proxyBackendApiRequest,
+  sanitizeSessionPayload
+} from './session-bff';
+
+describe('session BFF helpers', () => {
+  const previousBackendOrigin = process.env.BACKEND_ORIGIN;
+
+  afterEach(() => {
+    if (previousBackendOrigin === undefined) {
+      delete process.env.BACKEND_ORIGIN;
+    } else {
+      process.env.BACKEND_ORIGIN = previousBackendOrigin;
+    }
+    vi.unstubAllGlobals();
+  });
+
+  it('builds backend API URLs from the configured private origin', () => {
+    process.env.BACKEND_ORIGIN = 'http://backend.internal:1157';
+
+    expect(buildBackendApiUrl('/config/system', '?locale=en-US')).toBe(
+      'http://backend.internal:1157/api/config/system?locale=en-US'
+    );
+    
expect(buildBackendApiUrl('alerts')).toBe('http://backend.internal:1157/api/alerts');
+  });
+
+  it('strips credential material from login and refresh payloads before 
returning them to browser code', () => {
+    expect(
+      sanitizeSessionPayload({
+        code: 0,
+        data: {
+          token: 'access-token',
+          refreshToken: 'refresh-token',
+          user: 'admin'
+        }
+      })
+    ).toEqual({
+      code: 0,
+      data: {
+        user: 'admin',
+        authenticated: true,
+        tokenBoundary: 'bff-cookie'
+      }
+    });
+  });
+
+  it('forwards the BFF access cookie as backend authorization when the 
framework cookie reader misses it', async () => {
+    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
+      new Response(JSON.stringify({ code: 0, data: { ok: true } }), {
+        status: 200,
+        headers: { 'Content-Type': 'application/json' }
+      })
+    );
+    vi.stubGlobal('fetch', fetchMock);
+
+    const request = {
+      url: 'http://127.0.0.1:4200/api/config/system?locale=zh-CN',
+      method: 'GET',
+      headers: new Headers({
+        cookie: 'hb_ui_session=1; hb_ui_access=session-access-token; 
theme=dark',
+        'accept-language': 'zh-CN'
+      }),
+      cookies: {
+        get: () => undefined
+      },
+      arrayBuffer: vi.fn()
+    };
+
+    await proxyBackendApiRequest(request as any, '/config/system');
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      'http://127.0.0.1:1157/api/config/system?locale=zh-CN',
+      expect.objectContaining({
+        method: 'GET',
+        cache: 'no-store'
+      })
+    );
+    const forwardedHeaders = fetchMock.mock.calls[0][1]?.headers as Headers;
+    expect(forwardedHeaders.get('Authorization')).toBe('Bearer 
session-access-token');
+    expect(forwardedHeaders.get('cookie')).toBeNull();
+  });
+
+  it('does not forward backend auth challenges to browser-facing API 
responses', async () => {
+    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
+      new Response('token expired', {
+        status: 401,
+        headers: {
+          'Content-Type': 'text/plain',
+          'WWW-Authenticate': 'Digest realm=sureness_realm,nonce=abc,qop=auth'
+        }
+      })
+    );
+    vi.stubGlobal('fetch', fetchMock);
+
+    const request = {
+      url: 'http://127.0.0.1:4200/api/monitor',
+      method: 'GET',
+      headers: new Headers(),
+      cookies: {
+        get: () => undefined
+      },
+      arrayBuffer: vi.fn()
+    };
+
+    const response = await proxyBackendApiRequest(request as any, '/monitor');
+
+    expect(response.status).toBe(401);
+    expect(response.headers.get('WWW-Authenticate')).toBeNull();
+    expect(response.headers.get('Content-Type')).toContain('text/plain');
+  });
+
+  it('refreshes the BFF access cookie and retries protected API calls when 
only refresh session remains', async () => {
+    const fetchMock = vi.fn<typeof fetch>()
+      .mockResolvedValueOnce(
+        new Response('token expired', {
+          status: 401,
+          headers: {
+            'WWW-Authenticate': 'Digest 
realm=sureness_realm,nonce=abc,qop=auth'
+          }
+        })
+      )
+      .mockResolvedValueOnce(
+        new Response(
+          JSON.stringify({
+            code: 0,
+            data: {
+              token: 'new-access-token',
+              refreshToken: 'new-refresh-token'
+            }
+          }),
+          {
+            status: 200,
+            headers: { 'Content-Type': 'application/json' }
+          }
+        )
+      )
+      .mockResolvedValueOnce(
+        new Response(JSON.stringify({ code: 0, data: { ok: true } }), {
+          status: 200,
+          headers: { 'Content-Type': 'application/json' }
+        })
+      );
+    vi.stubGlobal('fetch', fetchMock);
+
+    const request = {
+      url: 'http://127.0.0.1:4200/api/config/system',
+      method: 'GET',
+      headers: new Headers({
+        cookie: 'hb_ui_session=1; hb_ui_refresh=session-refresh-token',
+        'accept-language': 'zh-CN'
+      }),
+      cookies: {
+        get: () => undefined
+      },
+      arrayBuffer: vi.fn()
+    };
+
+    const response = await proxyBackendApiRequest(request as any, 
'/config/system');
+
+    expect(response.status).toBe(200);
+    expect(await response.text()).toContain('"ok":true');
+    expect(fetchMock).toHaveBeenCalledTimes(3);
+    
expect(fetchMock.mock.calls[1][0]).toBe('http://127.0.0.1:1157/api/account/auth/refresh');
+    expect(fetchMock.mock.calls[1][1]).toEqual(
+      expect.objectContaining({
+        method: 'POST',
+        body: JSON.stringify({ token: 'session-refresh-token' })
+      })
+    );
+    const retryHeaders = fetchMock.mock.calls[2][1]?.headers as Headers;
+    expect(retryHeaders.get('Authorization')).toBe('Bearer new-access-token');
+    
expect(response.headers.get('set-cookie')).toContain('hb_ui_access=new-access-token');
+    
expect(response.headers.get('set-cookie')).toContain('hb_ui_refresh=new-refresh-token');
+    expect(response.headers.get('WWW-Authenticate')).toBeNull();
+  });
+});
diff --git a/web-next/lib/session-bff.ts b/web-next/lib/session-bff.ts
new file mode 100644
index 0000000000..b26af137a3
--- /dev/null
+++ b/web-next/lib/session-bff.ts
@@ -0,0 +1,206 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+export const HB_UI_ACCESS_COOKIE = 'hb_ui_access';
+export const HB_UI_REFRESH_COOKIE = 'hb_ui_refresh';
+export const HB_UI_SESSION_MARKER_COOKIE = 'hb_ui_session';
+
+type ApiPayload = {
+  code?: number;
+  msg?: string;
+  data?: Record<string, unknown> | null;
+};
+
+type SessionTokens = {
+  token?: string;
+  refreshToken?: string;
+};
+
+const DEFAULT_BACKEND_ORIGIN = 'http://127.0.0.1:1157';
+const ACCESS_MAX_AGE_SECONDS = 60 * 60;
+const REFRESH_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
+
+function baseCookieOptions(maxAge: number) {
+  return {
+    sameSite: 'lax' as const,
+    secure: process.env.NODE_ENV === 'production',
+    path: '/',
+    maxAge
+  };
+}
+
+function secretCookieOptions(maxAge: number) {
+  return {
+    ...baseCookieOptions(maxAge),
+    httpOnly: true
+  };
+}
+
+function markerCookieOptions(maxAge: number) {
+  return {
+    ...baseCookieOptions(maxAge),
+    httpOnly: false
+  };
+}
+
+function readCookieHeaderValue(cookieHeader: string | null, name: string) {
+  if (!cookieHeader) return undefined;
+  const prefix = `${name}=`;
+  for (const segment of cookieHeader.split(';')) {
+    const trimmed = segment.trim();
+    if (!trimmed.startsWith(prefix)) continue;
+    const rawValue = trimmed.slice(prefix.length);
+    if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
+      return rawValue.slice(1, -1);
+    }
+    return rawValue;
+  }
+  return undefined;
+}
+
+export function readSessionCookieValue(request: Pick<NextRequest, 'cookies' | 
'headers'>, name: string) {
+  return request.cookies.get(name)?.value ?? 
readCookieHeaderValue(request.headers.get('cookie'), name);
+}
+
+export function resolveBackendOrigin() {
+  return process.env.BACKEND_ORIGIN?.trim() || DEFAULT_BACKEND_ORIGIN;
+}
+
+export function buildBackendApiUrl(path: string, search = '') {
+  const cleanPath = path.startsWith('/') ? path : `/${path}`;
+  return `${resolveBackendOrigin()}/api${cleanPath}${search}`;
+}
+
+export function sanitizeSessionPayload(payload: ApiPayload): ApiPayload {
+  if (!payload.data || typeof payload.data !== 'object') {
+    return payload;
+  }
+  const { token: _token, refreshToken: _refreshToken, ...rest } = payload.data;
+  return {
+    ...payload,
+    data: {
+      ...rest,
+      authenticated: payload.code === 0,
+      tokenBoundary: 'bff-cookie'
+    }
+  };
+}
+
+export function applySessionCookies(response: NextResponse, tokens: 
SessionTokens) {
+  if (tokens.token) {
+    response.cookies.set(HB_UI_ACCESS_COOKIE, tokens.token, 
secretCookieOptions(ACCESS_MAX_AGE_SECONDS));
+  }
+  if (tokens.refreshToken) {
+    response.cookies.set(HB_UI_REFRESH_COOKIE, tokens.refreshToken, 
secretCookieOptions(REFRESH_MAX_AGE_SECONDS));
+  }
+  if (tokens.token || tokens.refreshToken) {
+    response.cookies.set(HB_UI_SESSION_MARKER_COOKIE, '1', 
markerCookieOptions(REFRESH_MAX_AGE_SECONDS));
+  }
+}
+
+export function clearSessionCookies(response: NextResponse) {
+  [HB_UI_ACCESS_COOKIE, HB_UI_REFRESH_COOKIE, 
HB_UI_SESSION_MARKER_COOKIE].forEach(name => {
+    const options = name === HB_UI_SESSION_MARKER_COOKIE ? 
markerCookieOptions(0) : secretCookieOptions(0);
+    response.cookies.set(name, '', options);
+  });
+}
+
+function copyProxyHeaders(request: NextRequest) {
+  const headers = new Headers(request.headers);
+  headers.delete('cookie');
+  headers.delete('host');
+  headers.delete('connection');
+  headers.delete('content-length');
+  return headers;
+}
+
+function copyResponseHeaders(headers: Headers) {
+  const nextHeaders = new Headers(headers);
+  nextHeaders.delete('content-encoding');
+  nextHeaders.delete('content-length');
+  nextHeaders.delete('transfer-encoding');
+  nextHeaders.delete('www-authenticate');
+  nextHeaders.delete('proxy-authenticate');
+  return nextHeaders;
+}
+
+async function readProxyBody(request: NextRequest) {
+  if (request.method === 'GET' || request.method === 'HEAD') {
+    return undefined;
+  }
+  return request.arrayBuffer();
+}
+
+async function refreshSessionTokens(request: NextRequest): 
Promise<SessionTokens | null> {
+  const refreshToken = readSessionCookieValue(request, HB_UI_REFRESH_COOKIE);
+  if (!refreshToken) {
+    return null;
+  }
+
+  const upstream = await fetch(buildBackendApiUrl('/account/auth/refresh'), {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      ...(request.headers.get('Accept-Language') ? { 'Accept-Language': 
request.headers.get('Accept-Language') as string } : {})
+    },
+    body: JSON.stringify({ token: refreshToken }),
+    cache: 'no-store'
+  });
+  const payload = await readJsonPayload(upstream);
+  if (!upstream.ok || payload.code !== 0 || !payload.data || typeof 
payload.data.token !== 'string') {
+    return null;
+  }
+
+  return {
+    token: payload.data.token,
+    refreshToken: typeof payload.data.refreshToken === 'string' ? 
payload.data.refreshToken : refreshToken
+  };
+}
+
+export async function proxyBackendApiRequest(request: NextRequest, path: 
string) {
+  const requestUrl = new URL(request.url);
+  const body = await readProxyBody(request);
+  const accessToken = readSessionCookieValue(request, HB_UI_ACCESS_COOKIE);
+
+  const fetchUpstream = (token?: string) => {
+    const headers = copyProxyHeaders(request);
+    if (token) {
+      headers.set('Authorization', `Bearer ${token}`);
+    }
+    return fetch(buildBackendApiUrl(path, requestUrl.search), {
+      method: request.method,
+      headers,
+      body,
+      redirect: 'manual',
+      cache: 'no-store'
+    });
+  };
+
+  let upstream = await fetchUpstream(accessToken);
+  let refreshedTokens: SessionTokens | null = null;
+  if (upstream.status === 401) {
+    refreshedTokens = await refreshSessionTokens(request);
+    if (refreshedTokens?.token) {
+      upstream = await fetchUpstream(refreshedTokens.token);
+    }
+  }
+
+  const response = new NextResponse(upstream.body, {
+    status: upstream.status,
+    statusText: upstream.statusText,
+    headers: copyResponseHeaders(upstream.headers)
+  });
+  if (refreshedTokens?.token && upstream.ok) {
+    applySessionCookies(response, refreshedTokens);
+  } else if (upstream.status === 401 && readSessionCookieValue(request, 
HB_UI_REFRESH_COOKIE)) {
+    clearSessionCookies(response);
+  }
+  return response;
+}
+
+export async function readJsonPayload(response: Response): Promise<ApiPayload> 
{
+  try {
+    return (await response.json()) as ApiPayload;
+  } catch {
+    return { code: response.ok ? 0 : response.status, msg: 
response.statusText, data: null };
+  }
+}


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

Reply via email to