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 3398db3c71f087c4301362fb666fea080dc3598f
Merge: b84cb3a73a f82c340a16
Author: Logic <[email protected]>
AuthorDate: Wed Aug 26 14:50:47 2026 +0800

    Merge validated React operational workflows

 .../manager/controller/MonitorController.java      |   14 +
 .../pojo/dto/MonitorInvestigationBindingInfo.java  |   36 +
 .../MonitorInvestigationReadModelService.java      |  117 ++
 .../manager/controller/MonitorControllerTest.java  |   44 +
 .../MonitorInvestigationReadModelServiceTest.java  |  160 +++
 .../controller/MetricsDataController.java          |   36 +
 .../controller/MetricsDataControllerTest.java      |   34 +
 web-app/package.json                               |    5 +
 web-app/playwright.config.ts                       |   41 +
 web-app/pnpm-lock.yaml                             |   57 +
 web-app/scripts/bundle-limits.json                 |    2 +-
 web-app/src/app/alert-route-source.test.tsx        |    1 +
 .../app/decorative-boundary-style-contract.test.ts |    3 +-
 .../monitor-navigation-resource-loader.test.tsx    |   17 +
 .../refine/monitor-navigation-resource-loader.tsx  |    6 +-
 .../app/refine/refine-resource-registry.test.ts    |    2 +-
 .../src/app/refine/refine-resource-registry.tsx    |   12 +-
 web-app/src/app/refine/refine-runtime.test.tsx     |    1 +
 web-app/src/app/refine/refine-runtime.tsx          |   20 +-
 .../src/app/refine/session-query-runtime.test.tsx  |   25 +
 web-app/src/app/refine/session-query-runtime.tsx   |   11 +-
 .../settings-form-layout-style-contract.test.ts    |   19 +-
 web-app/src/app/theme/hertzbeat-theme.test.ts      |   21 +-
 web-app/src/assets/i18n/en-us.json                 |   38 +
 web-app/src/assets/i18n/ja-jp.json                 |   38 +
 web-app/src/assets/i18n/pt-br.json                 |   38 +
 web-app/src/assets/i18n/zh-cn.json                 |   38 +
 web-app/src/assets/i18n/zh-tw.json                 |   38 +
 web-app/src/core/auth/session-cache-boundary.ts    |   17 +
 .../core/auth/session-foreground-revalidation.ts   |   21 +-
 web-app/src/core/i18n/locale-catalog-shape.test.ts |   52 +
 .../src/features/alert/api/alert-rule-api.test.ts  |    3 +-
 .../alert/api/alert-rule-write-proof.test.ts       |    3 +-
 .../components/alert-center-confirmed-action.tsx   |   45 -
 .../alert/components/alert-rule-type-chooser.tsx   |    5 +-
 .../use-alert-group-controller-create.test.tsx     |  264 +++++
 .../use-alert-group-controller-delete.test.tsx     |   97 ++
 .../use-alert-group-controller-ownership.test.tsx  |  247 +++++
 .../use-alert-group-controller-test-support.ts     |  167 +++
 .../use-alert-group-controller-update.test.tsx     |  404 +++++++
 .../controller/use-alert-group-controller.test.tsx | 1034 +-----------------
 .../controller/use-alert-rule-editor-route.ts      |    2 +-
 .../integration/model/alert-integration-model.ts   |    4 +-
 .../features/alert/model/alert-rule-condition.ts   |   13 -
 .../src/features/alert/model/alert-rule-draft.ts   |    4 +-
 .../features/alert/model/alert-silence-types.ts    |    2 +-
 .../model/notice-receiver-channel-validation.ts    |   37 +
 .../notice-receiver/model/notice-receiver-model.ts |   15 +-
 .../auth/pages/login-page-style-contract.test.ts   |   26 +-
 .../src/features/auth/pages/login-page.module.css  |   47 +-
 .../explore/controller/explore-query-keys.test.ts  |    2 +-
 .../explore/model/explore-context-model.ts         |    4 +
 .../features/explore/model/explore-model.test.ts   |   22 +
 .../src/features/explore/model/explore-query.ts    |   12 +
 .../features/explore/model/explore-url-model.ts    |   25 +-
 .../instrumentation-configure-step-props.ts        |   42 +
 .../components/instrumentation-configure-step.tsx  |  162 +--
 .../instrumentation-configure.module.css           |  138 +--
 ...instrumentation-destination-selector.module.css |  170 +++
 .../instrumentation-destination-selector.tsx       |  146 ++-
 .../instrumentation-guide-configuration.module.css |   68 ++
 .../instrumentation-guide-configuration.tsx        |  148 +++
 .../components/instrumentation-guide-workspace.tsx |   37 +-
 .../instrumentation-guided-configuration.test.tsx  |  160 +++
 .../instrumentation-onboarding.module.css          |   33 +-
 .../components/instrumentation-progress.tsx        |   21 +-
 .../components/instrumentation-shell.module.css    |   55 +-
 .../components/instrumentation-v2-flow.test.tsx    |   91 +-
 .../instrumentation/instrumentation-i18n.test.ts   |    9 +
 .../instrumentation/model/instrumentation-flow.ts  |    2 +-
 .../model/instrumentation-guided-flow.ts           |   24 +
 .../pages/instrumentation-page.test.tsx            |    5 +-
 .../instrumentation/pages/instrumentation-page.tsx |  140 +--
 .../pages/instrumentation-stage-content.tsx        |  130 +++
 .../src/features/monitor/api/monitor-api.test.ts   |    2 +-
 web-app/src/features/monitor/api/monitor-api.ts    |    8 +
 .../monitor/api/monitor-history-path.test.ts}      |   37 +-
 .../monitor/api/monitor-investigation-api.test.ts  |   62 ++
 .../monitor/api/monitor-investigation-schema.ts    |   47 +
 .../src/features/monitor/api/monitor-metric-api.ts |   11 +-
 .../monitor/api/monitor-metric-layout-api.test.ts  |  101 ++
 .../monitor/api/monitor-metric-layout-api.ts       |   17 +-
 .../monitor/components/monitor-list-results.tsx    |   35 +-
 .../monitor/components/monitor-list-view.test.tsx  |   15 +
 .../components/monitor-metric-workbench.test.tsx   |   48 +
 .../monitor-realtime-layout-grid.test.tsx          |   76 +-
 .../monitor-selected-history-controls.tsx          |   38 +-
 .../components/monitor-selected-history-tray.tsx   |    2 +-
 .../controller/monitor-metric-workbench-actions.ts |    3 +
 .../controller/monitor-metric-workbench-result.ts  |    5 +
 .../monitor/controller/monitor-query-keys.ts       |    1 +
 .../controller/use-monitor-investigation.ts        |   51 +
 .../use-monitor-metric-layout-command-owner.ts     |  103 ++
 .../use-monitor-metric-layout-controller.test.tsx  |  148 ++-
 .../use-monitor-metric-layout-controller.ts        |   69 +-
 ...se-monitor-metric-workbench-controller.test.tsx |   48 +
 .../use-monitor-metric-workbench-controller.ts     |    8 +-
 .../features/monitor/model/monitor-detail-model.ts |    3 +
 .../model/monitor-investigation-model.test.ts      |  112 ++
 .../monitor/model/monitor-investigation-model.ts   |  118 ++
 .../use-collector-controller-file-log.test.tsx     |  186 ++++
 .../use-collector-controller-list-intake.test.tsx  |  337 ++++++
 .../use-collector-controller-prometheus.test.tsx   |  248 +++++
 .../use-collector-controller-runtime.test.tsx      |  235 ++++
 .../use-collector-controller-test-support.ts       |  224 ++++
 .../controller/use-collector-controller.test.tsx   | 1124 +-------------------
 .../monitor-definition-catalog.module.css          |    2 +-
 ...onitor-definition-controller-authority.test.tsx |  328 ++++++
 ...monitor-definition-controller-recovery.test.tsx |  412 +++++++
 ...-monitor-definition-controller-routing.test.tsx |  240 +++++
 ...e-monitor-definition-controller-test-support.ts |  145 +++
 .../use-monitor-definition-controller.test.tsx     | 1038 +-----------------
 .../monitor-definition-layout-contract.test.ts     |    8 +
 .../system-config/api/public-access-config-api.ts  |    2 +-
 .../public-access-config-section.test.tsx          |   13 +-
 .../components/public-access-config-section.tsx    |   99 +-
 .../pages/system-config-page.test.tsx              |    9 +
 .../system-config/pages/system-config-page.tsx     |   14 +-
 web-app/src/features/setup/model/setup-optional.ts |    2 +-
 .../components/status-org-settings-workspace.tsx   |    2 +-
 ...status-management-controller-component.test.tsx |  371 +++++++
 ...-status-management-controller-incident.test.tsx |  124 +++
 ...-status-management-controller-resource.test.tsx |  354 ++++++
 ...se-status-management-controller-test-support.ts |  152 +++
 .../use-status-management-controller.test.tsx      |  929 +---------------
 .../controller/topology-query-keys.test.ts         |    2 +-
 .../topology/runtime/topology-g6-lifecycle.test.ts |   21 +
 .../topology/runtime/topology-g6-lifecycle.ts      |    5 +
 .../src/layout/shell/hertzbeat-shell.module.css    |    2 +-
 .../layout/shell/shell-navigation-style.test.ts    |    8 +
 .../notification-workspace-navigation.module.css   |    2 +-
 .../notification-workspace-style-contract.test.ts  |    7 +-
 .../query-context/query-context-model.test.ts      |    4 +-
 .../shared/query-context/query-context-model.ts    |   36 +
 web-app/src/shared/theme/hertzbeat-theme.ts        |   24 +
 web-app/src/test/locale-catalog-shape.ts           |   47 +
 .../tests/browser/production-accessibility.spec.ts |   61 ++
 .../tests/browser/production-browser-support.ts    |  335 ++++++
 .../tests/browser/production-route-smoke.spec.ts   |  244 +++++
 web-app/tsconfig.node.json                         |    2 +-
 web-app/vite.config.ts                             |    2 +-
 141 files changed, 8808 insertions(+), 4789 deletions(-)

diff --cc web-app/scripts/bundle-limits.json
index e99bd2eef9,eba1da0a5f..9cc392a5f1
--- a/web-app/scripts/bundle-limits.json
+++ b/web-app/scripts/bundle-limits.json
@@@ -3,5 -3,5 +3,5 @@@
    "vendorChunkMinBytes": 512000,
    "vendorChunkMaxBytes": 614400,
    "shellGzipBytes": 460800,
-   "totalJavaScriptBytes": 6690000
 -  "totalJavaScriptBytes": 6600000
++  "totalJavaScriptBytes": 7100000
  }
diff --cc web-app/src/app/alert-route-source.test.tsx
index 5d37938031,d33342118b..b52f52af71
--- a/web-app/src/app/alert-route-source.test.tsx
+++ b/web-app/src/app/alert-route-source.test.tsx
@@@ -166,6 -173,6 +166,7 @@@ describe('Alert route ownership', () =
          name: 'Canonical rule',
          expr: 'usage > 90',
          template: 'Alert',
++        labelsText: 'severity:warning',
          period: 300,
          times: 3
        })
diff --cc web-app/src/app/decorative-boundary-style-contract.test.ts
index cbf59ea9a5,cbf59ea9a5..96518ff37c
--- a/web-app/src/app/decorative-boundary-style-contract.test.ts
+++ b/web-app/src/app/decorative-boundary-style-contract.test.ts
@@@ -34,7 -34,7 +34,8 @@@ describe('decorative boundary style con
    });
  
    it('does not frame settings forms or table wrappers with decorative rules', 
() => {
--    expect(ruleBody(objectStoreStyles, 
/\.form\s*\{(?<body>[^}]*)\}/)).not.toMatch(horizontalBorder);
++    expect(ruleBody(objectStoreStyles, 
/\.workspace\s*\{(?<body>[^}]*)\}/)).not.toMatch(horizontalBorder);
++    expect(ruleBody(objectStoreStyles, 
/\.actions\s*\{(?<body>[^}]*)\}/)).not.toMatch(horizontalBorder);
      
expect(objectStoreStyles).not.toMatch(/\.field\s*\+\s*\.field\s*\{[^}]*border-top:/s);
      expect(ruleBody(systemConfigStyles, 
/\.form\s*\{(?<body>[^}]*)\}/)).not.toMatch(horizontalBorder);
      
expect(systemConfigStyles).not.toMatch(/\.field\s*\+\s*\.field\s*\{[^}]*border-top:/s);
diff --cc web-app/src/app/settings-form-layout-style-contract.test.ts
index c422a0a84e,c422a0a84e..b461e51c35
--- a/web-app/src/app/settings-form-layout-style-contract.test.ts
+++ b/web-app/src/app/settings-form-layout-style-contract.test.ts
@@@ -18,12 -18,12 +18,9 @@@ function ruleBody(css: string, selector
  }
  
  describe('settings form layout contract', () => {
--  it.each([
--    ['object storage', objectStoreStyles],
--    ['system settings', systemConfigStyles]
--  ])('keeps the %s controls in a compact stacked rail', (_name, styles) => {
--    const form = ruleBody(styles, /\.form\s*\{(?<body>[^}]*)\}/);
--    const field = ruleBody(styles, /\.field\s*\{(?<body>[^}]*)\}/);
++  it('keeps system settings controls in a compact stacked rail', () => {
++    const form = ruleBody(systemConfigStyles, /\.form\s*\{(?<body>[^}]*)\}/);
++    const field = ruleBody(systemConfigStyles, 
/\.field\s*\{(?<body>[^}]*)\}/);
  
      expect(form).toMatch(/max-width:\s*520px/);
      expect(form).toMatch(/margin-inline:\s*0/);
@@@ -31,6 -31,6 +28,16 @@@
      expect(field).not.toMatch(/190px/);
    });
  
++  it('keeps object storage as an adaptive provider workspace', () => {
++    const surface = ruleBody(objectStoreStyles, 
/\.surface\s*\{(?<body>[^}]*)\}/);
++    const workspace = ruleBody(objectStoreStyles, 
/\.workspace\s*\{(?<body>[^}]*)\}/);
++    const field = ruleBody(objectStoreStyles, /\.field\s*\{(?<body>[^}]*)\}/);
++
++    expect(surface).toMatch(/width:\s*min\(100%,\s*840px\)/);
++    expect(workspace).toMatch(/grid-template-columns:\s*238px 
minmax\(0,\s*1fr\)/);
++    expect(field).not.toMatch(/190px/);
++  });
++
    it('aligns default form actions with the left edge of the control rail', () 
=> {
      const actions = ruleBody(operationalPageStyles, 
/\.formActions\s*\{(?<body>[^}]*)\}/);
      expect(actions).toMatch(/justify-content:\s*flex-start/);
diff --cc web-app/src/features/alert/api/alert-rule-api.test.ts
index 52966807f8,52966807f8..e63a452847
--- a/web-app/src/features/alert/api/alert-rule-api.test.ts
+++ b/web-app/src/features/alert/api/alert-rule-api.test.ts
@@@ -235,7 -235,7 +235,8 @@@ describe('alert rule API', () => 
        ...createAlertRuleDraft(),
        name: 'CPU high',
        expr: 'usage > 90',
--      template: 'CPU'
++      template: 'CPU',
++      labelsText: 'severity:warning'
      };
  
      vi.mocked(apiMessageGet).mockRejectedValueOnce(transportFailure());
diff --cc web-app/src/features/alert/api/alert-rule-write-proof.test.ts
index 29ebe5d97b,29ebe5d97b..be27ca1b64
--- a/web-app/src/features/alert/api/alert-rule-write-proof.test.ts
+++ b/web-app/src/features/alert/api/alert-rule-write-proof.test.ts
@@@ -40,7 -40,7 +40,8 @@@ const expected = buildAlertRulePayload(
    ...createAlertRuleDraft(),
    name: 'New Rule',
    expr: 'usage > 90',
--  template: 'Alert'
++  template: 'Alert',
++  labelsText: 'severity:warning'
  });
  const matching: AlertRule = {
    id: 9,
diff --cc 
web-app/src/features/alert/components/alert-center-confirmed-action.tsx
index cac6a87f1c,3215783b76..0000000000
deleted file mode 100644,100644
--- a/web-app/src/features/alert/components/alert-center-confirmed-action.tsx
+++ /dev/null
@@@ -1,45 -1,43 +1,0 @@@
--/*
-- * Licensed to the Apache Software Foundation (ASF) under one or more
-- * contributor license agreements. See the NOTICE file distributed with
-- * this work for additional information regarding copyright ownership.
-- * The ASF licenses this file to You under the Apache License, Version 2.0.
-- */
--
--import { Button, Popconfirm } from 'antd';
--
--type AlertCenterConfirmedActionProps = {
-   className?: string;
--  confirm: string;
--  confirmLabel: string;
--  danger?: boolean;
--  disabled: boolean;
--  label: string;
--  run: () => void | Promise<unknown>;
--  type?: 'default' | 'link';
--};
--
--/** Centralizes confirmation semantics for destructive and status-changing 
alert actions. */
--export function AlertCenterConfirmedAction({
-   className,
--  confirm,
--  confirmLabel,
--  danger = false,
--  disabled,
--  label,
--  run,
--  type = 'default'
--}: AlertCenterConfirmedActionProps) {
--  return (
--    <Popconfirm
--      title={confirm}
--      okText={confirmLabel}
--      disabled={disabled}
--      okButtonProps={{ danger, disabled }}
--      onConfirm={() => !disabled && void run()}
--    >
-       <Button {...(className ? { className } : {})} size="small" type={type} 
danger={danger} disabled={disabled}>
 -      <Button size="small" type={type} danger={danger} disabled={disabled}>
--        {label}
--      </Button>
--    </Popconfirm>
--  );
--}
diff --cc web-app/src/features/alert/components/alert-rule-type-chooser.tsx
index d99e21d588,0000000000..5ca4ababb9
mode 100644,000000..100644
--- a/web-app/src/features/alert/components/alert-rule-type-chooser.tsx
+++ b/web-app/src/features/alert/components/alert-rule-type-chooser.tsx
@@@ -1,82 -1,0 +1,79 @@@
 +/* Licensed to the Apache Software Foundation (ASF) under the Apache License, 
Version 2.0. */
 +
 +import { ClockCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
 +import { Button, Modal } from 'antd';
 +import { useId } from 'react';
 +import { useTranslation } from 'react-i18next';
 +
 +import type { AlertRuleDatasourceState, AlertRuleKind } from 
'../model/alert-rule-model';
 +import styles from '../shared/alert-rule-editor.module.css';
 +import { AlertRuleDatasourceEvidence } from './alert-rule-editor-evidence';
 +
- export function AlertRuleTypeChooser(props: {
-   datasource: AlertRuleDatasourceState;
-   choose: (kind: AlertRuleKind) => void;
- }) {
++function AlertRuleTypeChooser(props: { datasource: AlertRuleDatasourceState; 
choose: (kind: AlertRuleKind) => void }) {
 +  const { t } = useTranslation();
 +  const realtimeTitleId = useId();
 +  const realtimeDescriptionId = useId();
 +  const periodicTitleId = useId();
 +  const periodicDescriptionId = useId();
 +  const periodicAvailable =
 +    props.datasource.kind === 'ready' &&
 +    (props.datasource.status.hasPromqlExecutor || 
props.datasource.status.hasSqlExecutor);
 +  return (
 +    <section className={styles.typeChooser} 
aria-label={t('alertRules.typeChoice.title')}>
 +      <Button
 +        aria-describedby={realtimeDescriptionId}
 +        aria-labelledby={realtimeTitleId}
 +        className={styles.typeChoice ?? ''}
 +        onClick={() => props.choose('realtime')}
 +      >
 +        <ThunderboltOutlined className={styles.typeChoiceIcon} />
 +        <span className={styles.typeChoiceContent}>
 +          <strong 
id={realtimeTitleId}>{t('alertRules.kind.realtime')}</strong>
 +          <small 
id={realtimeDescriptionId}>{t('alertRules.typeChoice.realtimeDescription')}</small>
 +        </span>
 +      </Button>
 +      <Button
 +        aria-describedby={periodicDescriptionId}
 +        aria-labelledby={periodicTitleId}
 +        className={styles.typeChoice ?? ''}
 +        disabled={!periodicAvailable}
 +        onClick={() => props.choose('periodic')}
 +      >
 +        <ClockCircleOutlined className={styles.typeChoiceIcon} />
 +        <span className={styles.typeChoiceContent}>
 +          <strong 
id={periodicTitleId}>{t('alertRules.kind.periodic')}</strong>
 +          <small id={periodicDescriptionId}>
 +            {t(
 +              periodicAvailable
 +                ? 'alertRules.typeChoice.periodicDescription'
 +                : 'alertRules.typeChoice.periodicUnavailable'
 +            )}
 +          </small>
 +        </span>
 +      </Button>
 +    </section>
 +  );
 +}
 +
 +export function AlertRuleTypeChooserDialog(props: {
 +  datasource: AlertRuleDatasourceState;
 +  onCancel: () => void;
 +  onChoose: (kind: AlertRuleKind) => void;
 +  retry: () => unknown;
 +}) {
 +  const { t } = useTranslation();
 +  return (
 +    <Modal open title={t('alertRules.typeChoice.title')} width={500} 
footer={null} onCancel={props.onCancel}>
 +      <div className={styles.typeChooserDialogBody}>
 +        <AlertRuleDatasourceEvidence state={props.datasource} 
retry={props.retry} />
 +        <AlertRuleTypeChooser
 +          datasource={props.datasource}
 +          choose={kind => {
 +            props.onChoose(kind);
 +            props.onCancel();
 +          }}
 +        />
 +      </div>
 +    </Modal>
 +  );
 +}
diff --cc web-app/src/features/alert/controller/use-alert-rule-editor-route.ts
index 91f78d7af1,09181c7b69..4c35540729
--- a/web-app/src/features/alert/controller/use-alert-rule-editor-route.ts
+++ b/web-app/src/features/alert/controller/use-alert-rule-editor-route.ts
@@@ -66,17 -62,6 +66,17 @@@ export function useAlertRuleEditorRoute
    };
  }
  
- export function parseAlertRuleKind(search: string): AlertRuleKind | null {
++function parseAlertRuleKind(search: string): AlertRuleKind | null {
 +  const value = new URLSearchParams(search).get('kind');
 +  return value === 'realtime' || value === 'periodic' ? value : null;
 +}
 +
 +function createInitialDraft(mode: 'new' | 'edit', kind: AlertRuleKind | null) 
{
 +  if (mode === 'edit' || kind === null) return null;
 +  const draft = createAlertRuleDraft();
 +  return { ...draft, ...buildAlertRuleStrategyPatch(draft, kind, 'metric') };
 +}
 +
  function resolveCanonicalDraft(
    mode: 'new' | 'edit',
    initialDraft: AlertRuleDraft | null,
diff --cc 
web-app/src/features/alert/integration/model/alert-integration-model.ts
index e6a0925fd0,650ca4409a..577323ced7
--- a/web-app/src/features/alert/integration/model/alert-integration-model.ts
+++ b/web-app/src/features/alert/integration/model/alert-integration-model.ts
@@@ -21,11 -21,7 +21,11 @@@ import { settingsPaths } from '@/shared
  
  const alertIntegrationWriterScope: AccessTokenScope = 'api-admin';
  
- export type AlertIntegrationReadiness = 'ready' | 'configuration_required' | 
'guide_blocked';
+ type AlertIntegrationReadiness = 'ready' | 'configuration_required' | 
'guide_blocked';
 +export type AlertIntegrationVerification =
 +  | { status: 'unverified'; startedAt: null; verifiedAt: null }
 +  | { status: 'waiting'; startedAt: number; verifiedAt: null }
 +  | { status: 'verified'; startedAt: number; verifiedAt: number };
  export type AlertIntegrationIconKey =
    | 'hertzbeat'
    | 'prometheus'
@@@ -43,13 -39,8 +43,13 @@@ type AlertIntegrationDescriptor = 
    readiness: AlertIntegrationReadiness;
    limitations: string[];
  };
 +export type AlertIntegrationCatalogItem = AlertIntegrationDescriptor & {
 +  verification: AlertIntegrationVerification;
 +};
  export type AlertIntegrationCatalog = { items: AlertIntegrationCatalogItem[] 
};
- export type AlertIntegrationRequiredHeaders =
 -export type AlertIntegrationGuide = AlertIntegrationCatalogItem & {
++type AlertIntegrationRequiredHeaders =
 +  { Authorization: 'Bearer {token}' } | { 'X-HertzBeat-Token': '{token}' } | 
{ Token: '{token}' };
 +export type AlertIntegrationGuide = AlertIntegrationDescriptor & {
    method: 'POST';
    ingressPath: string;
    payloadShape: string;
diff --cc web-app/src/features/alert/model/alert-rule-condition.ts
index cd71d49ef8,c7cf0eff26..4ba288311f
--- a/web-app/src/features/alert/model/alert-rule-condition.ts
+++ b/web-app/src/features/alert/model/alert-rule-condition.ts
@@@ -93,30 -75,19 +93,17 @@@ export function serializeMetricAlertCon
    );
  }
  
 -/**
 - * Returns null while a guided condition is incomplete. This lets the UI 
retain
 - * transient input without producing or discarding a writable rule.
 - */
 -export function serializeCompleteMetricAlertCondition(group: 
MetricAlertConditionGroup, fields: MetricAlertField[]) {
 -  if (group.items.length === 0) return null;
 -  try {
 -    return serializeMetricAlertCondition(group, fields);
 -  } catch {
 -    return null;
 -  }
 +/** Serializes the source-visible draft, including Angular-compatible 
incomplete values. */
 +export function serializeMetricAlertConditionAuthoring(group: 
MetricAlertConditionGroup, fields: MetricAlertField[]) {
 +  return serializeMetricAlertConditionAuthoringSource(
 +    group,
 +    metricFieldMap(fields),
 +    metricAlertOperatorsForType,
 +    metricAlertConditionLimits.maximumDepth,
 +    metricAlertConditionLimits.maximumItemsPerGroup
 +  );
  }
  
- /**
-  * Returns null while a guided condition is incomplete. This lets the UI 
retain
-  * transient input without producing or discarding a writable rule.
-  */
- export function serializeCompleteMetricAlertCondition(group: 
MetricAlertConditionGroup, fields: MetricAlertField[]) {
-   if (group.items.length === 0) return null;
-   try {
-     return serializeMetricAlertCondition(group, fields);
-   } catch {
-     return null;
-   }
- }
- 
  /**
   * Parses the structured subset conservatively. Unsupported or ambiguous input
   * returns null so callers can retain the source in expert mode.
diff --cc web-app/src/features/alert/model/alert-rule-draft.ts
index 9b58a669ed,39260b2829..4da79657c2
--- a/web-app/src/features/alert/model/alert-rule-draft.ts
+++ b/web-app/src/features/alert/model/alert-rule-draft.ts
@@@ -22,11 -19,6 +22,11 @@@ export type AlertRulePreviewRequest = 
    expr: string;
  };
  
 +export const alertRuleSeverities = ['emergency', 'critical', 'warning'] as 
const;
 +export const alertRuleModes = ['group', 'individual'] as const;
- export type AlertRuleSeverity = (typeof alertRuleSeverities)[number];
- export type AlertRuleMode = (typeof alertRuleModes)[number];
++type AlertRuleSeverity = (typeof alertRuleSeverities)[number];
++type AlertRuleMode = (typeof alertRuleModes)[number];
 +
  const strategyForType: Record<AlertRuleType, { kind: AlertRuleKind; dataType: 
AlertRuleDataType }> = {
    realtime_metric: { kind: 'realtime', dataType: 'metric' },
    periodic_metric: { kind: 'periodic', dataType: 'metric' },
diff --cc web-app/src/features/alert/model/alert-silence-types.ts
index 6ef27cb099,82dcdf827a..4657de5dba
--- a/web-app/src/features/alert/model/alert-silence-types.ts
+++ b/web-app/src/features/alert/model/alert-silence-types.ts
@@@ -20,16 -20,6 +20,16 @@@ import type { PagedCollection } from '@
  export type AlertSilenceQuery = { search: string; pageIndex: number; 
pageSize: number };
  export type AlertSilenceType = 0 | 1;
  
 +type AlertSilenceScheduleWindow = {
 +  periodStart: string;
 +  periodEnd: string;
 +};
 +
- export type AlertSilenceScheduleMemory = {
++type AlertSilenceScheduleMemory = {
 +  once: AlertSilenceScheduleWindow;
 +  recurring: AlertSilenceScheduleWindow;
 +};
 +
  type AlertSilenceWritableSnapshot = {
    matchAll: boolean;
    type: AlertSilenceType;
diff --cc 
web-app/src/features/alert/notice-receiver/model/notice-receiver-channel-validation.ts
index 0000000000,0000000000..215b67cf51
new file mode 100644
--- /dev/null
+++ 
b/web-app/src/features/alert/notice-receiver/model/notice-receiver-channel-validation.ts
@@@ -1,0 -1,0 +1,37 @@@
++/* Licensed to the Apache Software Foundation (ASF) under the Apache License, 
Version 2.0. */
++
++import type { NoticeReceiverDraft, NoticeReceiverSecretKey } from 
'./notice-receiver-model';
++
++type SecretAvailable = (draft: NoticeReceiverDraft, key: 
NoticeReceiverSecretKey) => boolean;
++
++export function channelValidationErrors(draft: NoticeReceiverDraft, 
secretAvailable: SecretAvailable) {
++  switch (draft.type) {
++    case 1:
++      return emailErrors(draft);
++    case 4:
++    case 5:
++      return phoneErrors(draft);
++    case 2:
++      return webhookAuthErrors(draft, secretAvailable);
++    case 10:
++      return weComRecipientErrors(draft);
++    default:
++      return [];
++  }
++}
++
++function emailErrors(draft: NoticeReceiverDraft) {
++  return draft.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(draft.email) ? 
['email'] : [];
++}
++
++function phoneErrors(draft: NoticeReceiverDraft) {
++  return draft.phone && !/^(1\d{10})(,\s*1\d{10})*$/.test(draft.phone) ? 
['phone'] : [];
++}
++
++function webhookAuthErrors(draft: NoticeReceiverDraft, secretAvailable: 
SecretAvailable) {
++  return draft.hookAuthType !== 'None' && !secretAvailable(draft, 
'hookAuthToken') ? ['hookAuthToken'] : [];
++}
++
++function weComRecipientErrors(draft: NoticeReceiverDraft) {
++  return [draft.userId, draft.partyId, draft.tagId].some(value => 
value.trim()) ? [] : ['recipientTarget'];
++}
diff --cc 
web-app/src/features/alert/notice-receiver/model/notice-receiver-model.ts
index dc340fdb7d,51dbd54430..a6f99dcbc0
--- a/web-app/src/features/alert/notice-receiver/model/notice-receiver-model.ts
+++ b/web-app/src/features/alert/notice-receiver/model/notice-receiver-model.ts
@@@ -21,6 -21,6 +21,8 @@@ import 
  import { compactTablePageSizes } from '@/shared/pagination';
  import { readZeroBasedPage, writeZeroBasedPage } from 
'@/shared/query-context';
  
++import { channelValidationErrors } from 
'./notice-receiver-channel-validation';
++
  export * from './notice-receiver-catalog';
  
  export const noticeReceiverPageSizes = compactTablePageSizes;
@@@ -146,7 -146,7 +148,7 @@@ export function validateNoticeReceiverD
    const invalid: string[] = normalizedName && normalizedName.length <= 
noticeReceiverNameMaxLength ? [] : ['name'];
    invalid.push(...requiredFieldErrors(draft));
    invalid.push(...activeValueErrors(draft));
--  invalid.push(...channelValidationErrors(draft));
++  invalid.push(...channelValidationErrors(draft, hasSecret));
    if (!hasUniqueSecrets(draft.configuredSecrets)) 
invalid.push('configuredSecrets');
    if (!hasUniqueSecrets(draft.clearSecrets)) invalid.push('clearSecrets');
    const recipientKey = draft.type === 14 ? 
feiShuRecipientKeys[draft.larkReceiveType] : undefined;
@@@ -176,17 -176,14 +178,6 @@@ function activeValueErrors(draft: Notic
    return invalid;
  }
  
--function channelValidationErrors(draft: NoticeReceiverDraft) {
--  if (draft.type === 1 && draft.email && 
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(draft.email)) return ['email'];
-   if ((draft.type === 4 || draft.type === 5) && draft.phone && 
!/^(1\d{10})(,\s*1\d{10})*$/.test(draft.phone)) {
-     return ['phone'];
-   }
--  if (draft.type === 2 && draft.hookAuthType !== 'None' && !hasSecret(draft, 
'hookAuthToken')) return ['hookAuthToken'];
--  if (draft.type === 10 && ![draft.userId, draft.partyId, 
draft.tagId].some(value => value.trim()))
--    return ['recipientTarget'];
--  return [];
--}
--
  export function buildNoticeReceiverPayload(draft: NoticeReceiverDraft) {
    requireValidNoticeReceiverDraft(draft);
    const options: Record<string, string | number | string[]> = {};
diff --cc web-app/src/features/monitor/components/monitor-list-results.tsx
index 802f2be094,ec2847fc6a..c42aefc3cf
--- a/web-app/src/features/monitor/components/monitor-list-results.tsx
+++ b/web-app/src/features/monitor/components/monitor-list-results.tsx
@@@ -79,17 -78,15 +79,7 @@@ export function MonitorListResults(
      return <OperationalStatePanel kind="unavailable" 
title={t('common.unavailable')} />;
    if (evidence.kind === 'error')
      return <OperationalStatePanel kind="error" 
title={t('common.routeError.description')} />;
--  const rowSelection: TableRowSelection<MonitorListRow> = {
--    selectedRowKeys: selectedIds,
-     getTitleCheckboxProps: () =>
-       pageSelectionTitleCheckboxProps(
-         selectedIds,
-         evidence.records.filter(record => 
!isMonitorRowDisappeared(record)).map(record => record.id),
-         pageSelectionLabels(t)
-       ),
-     getCheckboxProps: row => ({ disabled: operating || 
isMonitorRowDisappeared(row) }),
 -    getTitleCheckboxProps: () => ({ 'aria-label': 
t('monitorActions.selectAll') }),
 -    getCheckboxProps: row => ({
 -      'aria-label': t('monitorActions.selectOne', { name: row.name }),
 -      disabled: operating || isMonitorRowDisappeared(row)
 -    }),
--    onChange: keys => actions.selectIds(keys.flatMap(key => (typeof key === 
'number' ? [key] : [])))
--  };
++  const rowSelection = monitorRowSelection(t, evidence.records, selectedIds, 
operating, actions.selectIds);
    const selectionProps = canSelect ? { rowSelection } : {};
    return (
      <Table<MonitorListRow>
@@@ -112,6 -109,6 +102,29 @@@
    );
  }
  
++function monitorRowSelection(
++  t: TFunction,
++  records: readonly MonitorListRow[],
++  selectedIds: number[],
++  operating: boolean,
++  selectIds: MonitorResultActions['selectIds']
++): TableRowSelection<MonitorListRow> {
++  return {
++    selectedRowKeys: selectedIds,
++    getTitleCheckboxProps: () =>
++      pageSelectionTitleCheckboxProps(
++        selectedIds,
++        records.filter(record => !isMonitorRowDisappeared(record)).map(record 
=> record.id),
++        pageSelectionLabels(t)
++      ),
++    getCheckboxProps: row => ({
++      'aria-label': t('monitorActions.selectOne', { name: row.name }),
++      disabled: operating || isMonitorRowDisappeared(row)
++    }),
++    onChange: keys => selectIds(keys.flatMap(key => (typeof key === 'number' 
? [key] : [])))
++  };
++}
++
  function monitorTableChange(
    changeSort: (sort: MonitorQuery['sort'], order: MonitorQuery['order']) => 
void
  ): NonNullable<TableProps<MonitorListRow>['onChange']> {
diff --cc web-app/src/features/monitor/components/monitor-list-view.test.tsx
index 27d79bbe53,64b97f9a3e..67ac83a1bf
--- a/web-app/src/features/monitor/components/monitor-list-view.test.tsx
+++ b/web-app/src/features/monitor/components/monitor-list-view.test.tsx
@@@ -244,6 -244,21 +244,21 @@@ describe('MonitorListView evidence stat
      expect(actions.selectIds).not.toHaveBeenCalled();
    });
  
+   it('labels bulk and row selection controls with their exact scope', () => {
+     renderView({
+       monitors: {
+         kind: 'ready',
+         records: [{ id: 7, name: 'checkout-api', app: 'website', instance: 
'prod', status: 1 }],
+         total: 1
+       }
+     });
+ 
 -    expect(screen.getByRole('checkbox', { name: 
i18n.t('monitorActions.selectAll') })).toBeInTheDocument();
++    expect(screen.getByRole('checkbox', { name: 
i18n.t('common.tableSelection.selectAll') })).toBeInTheDocument();
+     expect(
+       screen.getByRole('checkbox', { name: i18n.t('monitorActions.selectOne', 
{ name: 'checkout-api' }) })
+     ).toBeInTheDocument();
+   });
+ 
    it('preserves endpoint copy, application drilldown, and discovery target 
semantics', () => {
      const copyInstance = vi.fn();
      const changeApp = vi.fn();
diff --cc web-app/src/features/monitor/controller/use-monitor-investigation.ts
index 0000000000,8bb2204ad0..5b7af62925
mode 000000,100644..100644
--- a/web-app/src/features/monitor/controller/use-monitor-investigation.ts
+++ b/web-app/src/features/monitor/controller/use-monitor-investigation.ts
@@@ -1,0 -1,58 +1,51 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements. See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0.
+  */
+ 
+ import { skipToken, useQuery } from '@tanstack/react-query';
+ import { useNavigate } from 'react-router-dom';
+ 
+ import type { SignalKind } from '@/shared/query-context';
+ import { loadMonitorInvestigationBinding } from '../api/monitor-api';
+ import type { Monitor } from '../model/monitor-contract';
 -import {
 -  monitorDetailRefreshInterval,
 -  type MonitorDetailRefreshSeconds,
 -  type MonitorHistoryChart
 -} from '../model/monitor-detail-model';
++import type { MonitorHistoryChart } from '../model/monitor-detail-model';
+ import {
+   buildMonitorInvestigationSignalPath,
+   createMonitorInvestigation,
+   monitorInvestigationWindow
+ } from '../model/monitor-investigation-model';
+ import { monitorQueryKeys } from './monitor-query-keys';
+ 
 -export function useMonitorInvestigation(
 -  monitor: Monitor | undefined,
 -  chart: MonitorHistoryChart | undefined,
 -  refreshSeconds: MonitorDetailRefreshSeconds
 -) {
++export function useMonitorInvestigation(monitor: Monitor | undefined, chart: 
MonitorHistoryChart | undefined) {
+   const navigate = useNavigate();
+   const query = useQuery({
+     queryKey: monitorQueryKeys.investigation(monitor?.id),
 -    queryFn: monitor ? ({ signal }) => 
loadMonitorInvestigationBinding(monitor.id, signal) : skipToken,
 -    refetchInterval: monitor ? monitorDetailRefreshInterval(refreshSeconds) : 
false,
++    queryFn: monitor
++      ? async ({ signal }) => (await 
loadMonitorInvestigationBinding(monitor.id, signal)) ?? null
++      : skipToken,
+     retry: false
+   });
+   const binding = query.data?.monitorId === monitor?.id ? query.data : 
undefined;
+   return {
+     signals: binding ? [...binding.signals] : [],
+     open: (signal: SignalKind) => {
+       if (!monitor || !binding || !chart || 
!binding.signals.includes(signal)) return;
+       try {
+         const window = monitorInvestigationWindow(
+           chart.history,
+           Date.now(),
+           Intl.DateTimeFormat().resolvedOptions().timeZone
+         );
+         const investigation = createMonitorInvestigation(monitor, binding, 
window);
+         const path = buildMonitorInvestigationSignalPath(investigation, 
signal);
+         if (path) void navigate(path);
+       } catch {
+         // Invalid local time or stale identity must not create a partial 
handoff.
+       }
+     },
 -    refetch: query.refetch
++    refetch: () => (monitor ? query.refetch() : Promise.resolve())
+   };
+ }
diff --cc 
web-app/src/features/monitor/controller/use-monitor-metric-workbench-controller.ts
index 3ed6765562,bf0cbb4d66..26da2ec1db
--- 
a/web-app/src/features/monitor/controller/use-monitor-metric-workbench-controller.ts
+++ 
b/web-app/src/features/monitor/controller/use-monitor-metric-workbench-controller.ts
@@@ -68,6 -69,7 +69,7 @@@ function useMonitorMetricContext
    const layout = 
useMonitorMetricLayoutController(monitorLayoutApplication(monitor), 
realtimeSelection.names);
    const historyCharts = buildHistoryChartEvidence(historySelection, queries);
    const selectedHistoryChart = buildSelectedHistoryChart(metric, 
historySelection, queries);
 -  const investigation = useMonitorInvestigation(monitor, 
selectedHistoryChart, options.refreshControl.refreshSeconds);
++  const investigation = useMonitorInvestigation(monitor, 
selectedHistoryChart);
    const actions = buildWorkbenchActions({
      monitor,
      metric,
diff --cc 
web-app/src/features/settings/system-config/api/public-access-config-api.ts
index 22e51bc8a5,0000000000..770c6d7f7f
mode 100644,000000..100644
--- 
a/web-app/src/features/settings/system-config/api/public-access-config-api.ts
+++ 
b/web-app/src/features/settings/system-config/api/public-access-config-api.ts
@@@ -1,23 -1,0 +1,23 @@@
 +/* Licensed to the Apache Software Foundation (ASF) under the Apache License, 
Version 2.0. */
 +
 +import { apiMessageGet, apiMessagePost } from '@/core/http/api-message';
 +
 +import type { PublicAccessConfig } from '../model/public-access-config-model';
 +import { parsePublicAccessConfig } from './public-access-config-schema';
 +
- export const publicAccessConfigEndpoint = '/api/config/public-access';
++const publicAccessConfigEndpoint = '/api/config/public-access';
 +export const publicAccessConfigQueryKey = ['settings', 'public-access'] as 
const;
 +
 +export async function loadPublicAccessConfig(signal?: AbortSignal) {
 +  const value = signal
 +    ? await apiMessageGet(publicAccessConfigEndpoint, { signal })
 +    : await apiMessageGet(publicAccessConfigEndpoint);
 +  return parsePublicAccessConfig(value);
 +}
 +
 +export async function savePublicAccessConfig(config: PublicAccessConfig, 
signal?: AbortSignal) {
 +  const value = signal
 +    ? await apiMessagePost(publicAccessConfigEndpoint, config, { signal })
 +    : await apiMessagePost(publicAccessConfigEndpoint, config);
 +  return parsePublicAccessConfig(value);
 +}
diff --cc 
web-app/src/features/settings/system-config/components/public-access-config-section.test.tsx
index 4110f1a046,0000000000..032c897573
mode 100644,000000..100644
--- 
a/web-app/src/features/settings/system-config/components/public-access-config-section.test.tsx
+++ 
b/web-app/src/features/settings/system-config/components/public-access-config-section.test.tsx
@@@ -1,67 -1,0 +1,68 @@@
 +/* Licensed to the Apache Software Foundation (ASF) under the Apache License, 
Version 2.0. */
 +
 +// @vitest-environment jsdom
 +
 +import { cleanup, fireEvent, render, screen } from '@testing-library/react';
 +import { afterEach, describe, expect, it, vi } from 'vitest';
 +
 +const controller = vi.hoisted(() => ({
 +  actions: { discard: vi.fn(), retry: vi.fn(), save: vi.fn(), update: vi.fn() 
},
 +  state: {
 +    kind: 'ready' as const,
 +    current: {
 +      publicBaseUrl: 'https://hertzbeat.example.test/ops',
 +      serverOtlpHttpEndpoint: '',
 +      serverOtlpGrpcEndpoint: ''
 +    },
 +    dirty: false,
 +    saving: false,
 +    valid: true
 +  }
 +}));
 +
- vi.mock('../controller/use-public-access-config-controller', () => ({
-   usePublicAccessConfigController: () => controller
- }));
 +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) 
=> key }) }));
 +
 +import { PublicAccessConfigSection } from './public-access-config-section';
 +
 +describe('PublicAccessConfigSection', () => {
 +  afterEach(() => {
 +    cleanup();
 +    controller.state.current.serverOtlpHttpEndpoint = '';
 +    controller.state.current.serverOtlpGrpcEndpoint = '';
 +    vi.clearAllMocks();
 +  });
 +
 +  it('keeps OTLP overrides collapsed until the operator opens advanced 
settings', () => {
-     render(<PublicAccessConfigSection canConfigure />);
++    renderSection();
 +
 +    const summary = 
screen.getByText('systemConfig.publicAccess.advancedTitle').closest('summary');
 +    const details = summary?.closest('details');
 +    expect(details).not.toHaveAttribute('open');
 +
 +    fireEvent.click(summary!);
 +
 +    expect(details).toHaveAttribute('open');
 +    
expect(screen.getByText('systemConfig.publicAccess.otlpHttp')).toBeInTheDocument();
 +    
expect(screen.getByText('systemConfig.publicAccess.otlpGrpc')).toBeInTheDocument();
 +  });
 +
 +  it('shows derived defaults as placeholders without turning them into 
persisted overrides', () => {
-     render(<PublicAccessConfigSection canConfigure />);
++    renderSection();
 +
 +    
expect(screen.getByPlaceholderText('https://hertzbeat.example.test:4318')).toHaveValue('');
 +    
expect(screen.getByPlaceholderText('https://hertzbeat.example.test:4317')).toHaveValue('');
 +    expect(controller.actions.update).not.toHaveBeenCalled();
 +  });
 +
 +  it('opens advanced settings when an OTLP override is already configured', 
() => {
 +    controller.state.current.serverOtlpHttpEndpoint = 
'https://collector.example.test/v1/metrics';
 +
-     render(<PublicAccessConfigSection canConfigure />);
++    renderSection();
 +
 +    
expect(screen.getByText('systemConfig.publicAccess.advancedTitle').closest('details')).toHaveAttribute('open');
 +  });
 +});
++
++function renderSection() {
++  return render(<PublicAccessConfigSection canConfigure 
state={controller.state} actions={controller.actions} />);
++}
diff --cc 
web-app/src/features/settings/system-config/components/public-access-config-section.tsx
index a08365ff13,0000000000..19e3e82d0f
mode 100644,000000..100644
--- 
a/web-app/src/features/settings/system-config/components/public-access-config-section.tsx
+++ 
b/web-app/src/features/settings/system-config/components/public-access-config-section.tsx
@@@ -1,158 -1,0 +1,193 @@@
 +/* Licensed to the Apache Software Foundation (ASF) under the Apache License, 
Version 2.0. */
 +
 +import { Button, Input, Typography } from 'antd';
 +import { RightOutlined } from '@ant-design/icons';
 +import { useState } from 'react';
 +import { useTranslation } from 'react-i18next';
 +
 +import { OperationalFormActions, OperationalStatePanel } from 
'@/shared/operational-page';
 +
- import { usePublicAccessConfigController } from 
'../controller/use-public-access-config-controller';
 +import { derivePublicAccessEndpoints, type PublicAccessConfigDraft } from 
'../model/public-access-config-model';
 +import styles from './system-config-editor.module.css';
 +
- export function PublicAccessConfigSection({ canConfigure }: { canConfigure: 
boolean }) {
++type PublicAccessConfigSectionState =
++  | { kind: 'loading' }
++  | { kind: 'unavailable' | 'invalid' }
++  | {
++      kind: 'ready';
++      current: PublicAccessConfigDraft;
++      dirty: boolean;
++      saving: boolean;
++      valid: boolean;
++    };
++
++type PublicAccessConfigSectionActions = {
++  discard: () => unknown;
++  retry: () => unknown;
++  save: () => unknown;
++  update: (field: keyof PublicAccessConfigDraft, value: string) => void;
++};
++
++export function PublicAccessConfigSection({
++  canConfigure,
++  state,
++  actions
++}: {
++  canConfigure: boolean;
++  state: PublicAccessConfigSectionState;
++  actions: PublicAccessConfigSectionActions;
++}) {
 +  const { t } = useTranslation();
-   const controller = usePublicAccessConfigController(canConfigure);
-   const state = controller.state;
 +
 +  return (
 +    <section className={styles.section} 
aria-labelledby="public-access-config-title">
 +      <header className={styles.sectionHeading}>
 +        <Typography.Title id="public-access-config-title" level={3}>
 +          {t('systemConfig.publicAccess.title')}
 +        </Typography.Title>
 +        <Typography.Paragraph 
type="secondary">{t('systemConfig.publicAccess.description')}</Typography.Paragraph>
 +      </header>
 +      {state.kind === 'loading' && (
 +        <OperationalStatePanel kind="loading" 
title={t('systemConfig.publicAccess.loading')} presentation="quiet" />
 +      )}
 +      {(state.kind === 'unavailable' || state.kind === 'invalid') && (
 +        <OperationalStatePanel
 +          kind={state.kind === 'invalid' ? 'error' : 'unavailable'}
 +          title={t(`systemConfig.publicAccess.${state.kind}`)}
 +          action={
-             <Button size="small" onClick={controller.actions.retry}>
++            <Button size="small" onClick={actions.retry}>
 +              {t('common.retry')}
 +            </Button>
 +          }
 +          presentation="quiet"
 +        />
 +      )}
 +      {state.kind === 'ready' && (
-         <>
-           <div className={styles.form}>
-             <PublicAddressField
-               field="publicBaseUrl"
-               label={t('systemConfig.publicAccess.publicBaseUrl')}
-               help={t('systemConfig.publicAccess.publicBaseUrlHelp')}
-               value={state.current.publicBaseUrl}
-               disabled={!canConfigure || state.saving}
-               update={controller.actions.update}
-             />
-             <OtlpOverrideFields
-               draft={state.current}
-               disabled={!canConfigure || state.saving}
-               update={controller.actions.update}
-             />
-           </div>
-           {canConfigure && state.dirty && (
-             <OperationalFormActions>
-               <Button type="primary" loading={state.saving} 
disabled={!state.valid} onClick={controller.actions.save}>
-                 {t('common.save')}
-               </Button>
-               <Button disabled={state.saving} 
onClick={controller.actions.discard}>
-                 {t('systemConfig.discard')}
-               </Button>
-             </OperationalFormActions>
-           )}
-         </>
++        <PublicAccessReadySection canConfigure={canConfigure} state={state} 
actions={actions} />
 +      )}
 +    </section>
 +  );
 +}
 +
++function PublicAccessReadySection({
++  canConfigure,
++  state,
++  actions
++}: {
++  canConfigure: boolean;
++  state: Extract<PublicAccessConfigSectionState, { kind: 'ready' }>;
++  actions: PublicAccessConfigSectionActions;
++}) {
++  const { t } = useTranslation();
++  const disabled = !canConfigure || state.saving;
++  return (
++    <>
++      <div className={styles.form}>
++        <PublicAddressField
++          field="publicBaseUrl"
++          label={t('systemConfig.publicAccess.publicBaseUrl')}
++          help={t('systemConfig.publicAccess.publicBaseUrlHelp')}
++          value={state.current.publicBaseUrl}
++          disabled={disabled}
++          update={actions.update}
++        />
++        <OtlpOverrideFields draft={state.current} disabled={disabled} 
update={actions.update} />
++      </div>
++      {canConfigure && state.dirty && (
++        <OperationalFormActions>
++          <Button type="primary" loading={state.saving} 
disabled={!state.valid} onClick={actions.save}>
++            {t('common.save')}
++          </Button>
++          <Button disabled={state.saving} onClick={actions.discard}>
++            {t('systemConfig.discard')}
++          </Button>
++        </OperationalFormActions>
++      )}
++    </>
++  );
++}
++
 +function OtlpOverrideFields({
 +  draft,
 +  disabled,
 +  update
 +}: {
 +  draft: PublicAccessConfigDraft;
 +  disabled: boolean;
 +  update: (field: keyof PublicAccessConfigDraft, value: string) => void;
 +}) {
 +  const { t } = useTranslation();
 +  const defaults = derivePublicAccessEndpoints(draft.publicBaseUrl);
 +  const [advancedOpen, setAdvancedOpen] = useState(() =>
 +    Boolean(draft.serverOtlpHttpEndpoint || draft.serverOtlpGrpcEndpoint)
 +  );
 +
 +  return (
 +    <details
 +      className={styles.advanced}
 +      open={advancedOpen}
 +      onToggle={event => setAdvancedOpen(event.currentTarget.open)}
 +    >
 +      <summary className={styles.advancedSummary}>
 +        <RightOutlined className={styles.advancedChevron} aria-hidden />
 +        <span className={styles.advancedCopy}>
 +          <span 
className={styles.advancedTitle}>{t('systemConfig.publicAccess.advancedTitle')}</span>
 +          <Typography.Text 
type="secondary">{t('systemConfig.publicAccess.advancedHelp')}</Typography.Text>
 +        </span>
 +      </summary>
 +      <div className={styles.advancedFields}>
 +        <PublicAddressField
 +          field="serverOtlpHttpEndpoint"
 +          label={t('systemConfig.publicAccess.otlpHttp')}
 +          help={t('systemConfig.publicAccess.otlpHttpHelp')}
 +          value={draft.serverOtlpHttpEndpoint}
 +          {...(defaults?.http ? { placeholder: defaults.http } : {})}
 +          disabled={disabled}
 +          update={update}
 +        />
 +        <PublicAddressField
 +          field="serverOtlpGrpcEndpoint"
 +          label={t('systemConfig.publicAccess.otlpGrpc')}
 +          help={t('systemConfig.publicAccess.otlpGrpcHelp')}
 +          value={draft.serverOtlpGrpcEndpoint}
 +          {...(defaults?.grpc ? { placeholder: defaults.grpc } : {})}
 +          disabled={disabled}
 +          update={update}
 +        />
 +      </div>
 +    </details>
 +  );
 +}
 +
 +function PublicAddressField({
 +  field,
 +  label,
 +  help,
 +  value,
 +  placeholder,
 +  disabled,
 +  update
 +}: {
 +  field: keyof PublicAccessConfigDraft;
 +  label: string;
 +  help: string;
 +  value: string;
 +  placeholder?: string;
 +  disabled: boolean;
 +  update: (field: keyof PublicAccessConfigDraft, value: string) => void;
 +}) {
 +  return (
 +    <label className={styles.field}>
 +      <span className={styles.label}>{label}</span>
 +      <span className={styles.control}>
 +        <Input
 +          value={value}
 +          placeholder={placeholder}
 +          disabled={disabled}
 +          onChange={event => update(field, event.target.value)}
 +        />
 +        <Typography.Text type="secondary">{help}</Typography.Text>
 +      </span>
 +    </label>
 +  );
 +}
diff --cc 
web-app/src/features/settings/system-config/pages/system-config-page.test.tsx
index eddf75b4b6,1df176b947..0bd1b122ad
--- 
a/web-app/src/features/settings/system-config/pages/system-config-page.test.tsx
+++ 
b/web-app/src/features/settings/system-config/pages/system-config-page.test.tsx
@@@ -34,12 -34,9 +34,20 @@@ const controller = vi.hoisted(() => (
    useCurrentServerSettings: vi.fn(),
    useSystemConfigResourceController: vi.fn()
  }));
++const publicAccessController = vi.hoisted(() => ({
++  actions: { discard: vi.fn(), retry: vi.fn(), save: vi.fn(), update: vi.fn() 
},
++  state: { kind: 'loading' as const },
++  usePublicAccessConfigController: vi.fn()
++}));
  vi.mock('../controller/system-config-resource-controller', () => ({
    useSystemConfigResourceController: 
controller.useSystemConfigResourceController
  }));
++vi.mock('../controller/use-public-access-config-controller', () => ({
++  usePublicAccessConfigController: 
publicAccessController.usePublicAccessConfigController
++}));
 +vi.mock('../components/public-access-config-section', () => ({
 +  PublicAccessConfigSection: () => <section aria-label="Public access 
settings" />
 +}));
  
  import { SystemConfigPage } from './system-config-page';
  
@@@ -53,6 -50,6 +61,7 @@@ describe('SystemConfigPage', () => 
    beforeEach(() => {
      vi.clearAllMocks();
      
controller.useSystemConfigResourceController.mockReturnValue(buildController());
++    
publicAccessController.usePublicAccessConfigController.mockReturnValue(publicAccessController);
    });
    afterEach(cleanup);
  
diff --cc 
web-app/src/features/settings/system-config/pages/system-config-page.tsx
index 562c0f3286,28b328f248..771d15394c
--- a/web-app/src/features/settings/system-config/pages/system-config-page.tsx
+++ b/web-app/src/features/settings/system-config/pages/system-config-page.tsx
@@@ -27,9 -27,8 +27,10 @@@ import 
  } from '@/shared/operational-page';
  
  import { SystemConfigEditor } from '../components/system-config-editor';
 +import { PublicAccessConfigSection } from 
'../components/public-access-config-section';
  import { RetryButton, SystemConfigReadFailure } from 
'../components/system-config-state';
  import { useSystemConfigResourceController } from 
'../controller/system-config-resource-controller';
++import { usePublicAccessConfigController } from 
'../controller/use-public-access-config-controller';
  
  export function SystemConfigPage() {
    const { t } = useTranslation();
@@@ -58,8 -57,8 +59,15 @@@ function SystemConfigContent({ controll
  }
  
  function SystemConfigReadyContent({ controller }: { controller: 
SystemConfigController }) {
++  const { state } = controller;
++  if (state.kind !== 'ready') return null;
++  return <SystemConfigReadyBody controller={controller} />;
++}
++
++function SystemConfigReadyBody({ controller }: { controller: 
SystemConfigController }) {
    const { t } = useTranslation();
    const { state } = controller;
++  const publicAccessController = usePublicAccessConfigController(state.kind 
=== 'ready' && state.canConfigure);
    if (state.kind !== 'ready') return null;
    return (
      <>
@@@ -94,7 -93,6 +102,11 @@@
          onSave={controller.save}
          onDiscard={controller.discard}
        />
-       <PublicAccessConfigSection canConfigure={state.canConfigure} />
++      <PublicAccessConfigSection
++        canConfigure={state.canConfigure}
++        state={publicAccessController.state}
++        actions={publicAccessController.actions}
++      />
      </>
    );
  }
diff --cc web-app/src/features/setup/model/setup-optional.ts
index bc9254b26f,0723b00c6c..632e24f830
--- a/web-app/src/features/setup/model/setup-optional.ts
+++ b/web-app/src/features/setup/model/setup-optional.ts
@@@ -110,47 -113,12 +110,47 @@@ export function clearOptionalMailSecret
    return { ...draft, mail: { ...draft.mail, password: '' } };
  }
  
 -function normalizedPublicAccess(draft: SetupOptionalDraft): SetupPublicAccess 
{
 -  return compactStrings({
 -    publicBaseUrl: draft.publicBaseUrl,
 -    serverOtlpHttpEndpoint: draft.serverOtlpHttpEndpoint,
 -    serverOtlpGrpcEndpoint: draft.serverOtlpGrpcEndpoint
 -  });
 +function normalizedPublicAccess(draft: SetupOptionalDraft, publicOrigin: 
string): SetupPublicAccess {
 +  const candidate = draft.useProxy ? draft.proxyPublicBaseUrl : publicOrigin;
 +  const publicBaseUrl = normalizeSetupPublicBaseUrl(candidate);
 +  if (!publicBaseUrl) throw new Error('Invalid setup public base URL');
 +  return { publicBaseUrl };
 +}
 +
- export function setupPublicAccessValid(draft: SetupOptionalDraft) {
++function setupPublicAccessValid(draft: SetupOptionalDraft) {
 +  return !draft.useProxy || 
normalizeSetupPublicBaseUrl(draft.proxyPublicBaseUrl) !== null;
 +}
 +
 +export function setupPublicAccessValidationReady(draft: SetupOptionalDraft) {
 +  return setupPublicAccessValid(draft);
 +}
 +
 +function normalizeSetupPublicBaseUrl(value: string) {
 +  const candidate = value.trim();
 +  if (!candidate) return null;
 +  try {
 +    const address = new URL(candidate);
 +    if (
 +      (address.protocol !== 'http:' && address.protocol !== 'https:') ||
 +      !address.hostname ||
 +      address.username ||
 +      address.password ||
 +      address.search ||
 +      address.hash ||
 +      address.port === '0' ||
 +      wildcardHost(address.hostname)
 +    ) {
 +      return null;
 +    }
 +    return candidate;
 +  } catch {
 +    return null;
 +  }
 +}
 +
 +function wildcardHost(value: string) {
 +  const host = value.toLowerCase().replace(/^\[|\]$/g, '');
 +  return host === '0.0.0.0' || host === '::' || host === '0:0:0:0:0:0:0:0';
  }
  
  function normalizedMail(mail: SetupOptionalDraft['mail']): SetupMail {
diff --cc 
web-app/src/features/status/management/components/status-org-settings-workspace.tsx
index 16fa6f38ae,0000000000..f2e9a8cccb
mode 100644,000000..100644
--- 
a/web-app/src/features/status/management/components/status-org-settings-workspace.tsx
+++ 
b/web-app/src/features/status/management/components/status-org-settings-workspace.tsx
@@@ -1,105 -1,0 +1,105 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements. See the NOTICE file distributed with
 + * this work for additional information regarding copyright ownership.
 + * The ASF licenses this file to You under the Apache License, Version 2.0
 + * (the "License"); you may not use this file except in compliance with
 + * the License. You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +
 +import { Form } from 'antd';
 +import type { FormInstance } from 'antd';
 +import type { ReactNode } from 'react';
 +import { useTranslation } from 'react-i18next';
 +
 +import type { StatusOrg } from '../model/status-management-contract';
 +import styles from './status-org-settings.module.css';
 +import { StatusOrgSummary } from './status-org-presentation';
 +import { StatusWriteRecoveryAlert } from './status-write-recovery-alert';
 +
 +interface ConfiguredWorkspaceProps {
 +  org: StatusOrg;
 +  editing: boolean;
 +  actions: ReactNode;
 +  fields: ReactNode;
 +  writeRecovery: 'proof' | 'commit-uncertain' | undefined;
 +}
 +
 +interface FormWorkspaceProps extends Omit<ConfiguredWorkspaceProps, 'org'> {
 +  org: StatusOrg | undefined;
 +  form: FormInstance<StatusOrg>;
 +  onFinish: (value: StatusOrg) => void;
 +}
 +
 +export function StatusOrgFormWorkspace(props: FormWorkspaceProps) {
 +  const configured = props.org ? <StatusOrgConfiguredWorkspace {...props} 
org={props.org} /> : undefined;
 +  if (configured && !props.editing) return configured;
 +  return (
 +    <Form form={props.form} layout="vertical" onFinish={props.onFinish}>
 +      {configured ?? (
 +        <StatusOrgSetupWorkspace actions={props.actions} 
fields={props.fields} writeRecovery={props.writeRecovery} />
 +      )}
 +    </Form>
 +  );
 +}
 +
 +function StatusOrgConfiguredWorkspace(props: ConfiguredWorkspaceProps) {
 +  const content = props.editing ? (
 +    <>
 +      {props.writeRecovery && <StatusWriteRecoveryAlert />}
 +      <div className={styles.settingsBody}>{props.fields}</div>
 +    </>
 +  ) : (
 +    <div className={styles.settingsBody}>
 +      <StatusOrgSummary org={props.org} />
 +    </div>
 +  );
 +  return (
 +    <div className={styles.configurationWorkspace}>
 +      <StatusOrgSettingsPanel 
actions={props.actions}>{content}</StatusOrgSettingsPanel>
 +    </div>
 +  );
 +}
 +
- export function StatusOrgSetupWorkspace({
++function StatusOrgSetupWorkspace({
 +  actions,
 +  fields,
 +  writeRecovery
 +}: {
 +  actions: ReactNode;
 +  fields: ReactNode;
 +  writeRecovery: 'proof' | 'commit-uncertain' | undefined;
 +}) {
 +  return (
 +    <div className={styles.setupWorkspace}>
 +      <div className={styles.setupForm}>
 +        {writeRecovery && <StatusWriteRecoveryAlert />}
 +        {fields}
 +        {actions}
 +      </div>
 +    </div>
 +  );
 +}
 +
 +function StatusOrgSettingsPanel({ actions, children }: { actions: ReactNode; 
children: ReactNode }) {
 +  const { t } = useTranslation();
 +  return (
 +    <section className={styles.settingsPanel} 
aria-label={t('statusManagement.organization')}>
 +      <div className={styles.settingsContent}>
 +        <header className={styles.settingsContentHeader}>
 +          <h3>{t('statusManagement.organization')}</h3>
 +          {actions}
 +        </header>
 +        {children}
 +      </div>
 +    </section>
 +  );
 +}
diff --cc web-app/src/layout/shell/shell-navigation-style.test.ts
index 2fdf4112d0,3d0b650508..a1fbbca664
--- a/web-app/src/layout/shell/shell-navigation-style.test.ts
+++ b/web-app/src/layout/shell/shell-navigation-style.test.ts
@@@ -23,14 -23,11 +23,22 @@@ describe('shell navigation visual hiera
      
expect(shellStyles).toMatch(/\.navigationParentOpen\s*\{[^}]*background:\s*var\(--hb-nav-hover\)/);
    });
  
 +  it('animates accordion height and chevrons while respecting reduced-motion 
preferences', () => {
 +    expect(shellStyles).toMatch(
 +      
/\.navigationChildrenMotion\s*\{[^}]*grid-template-rows:\s*0fr[^}]*transition:[^}]*grid-template-rows/s
 +    );
 +    
expect(shellStyles).toMatch(/\.navigationChildrenMotion\[data-open='true'\]\s*\{[^}]*grid-template-rows:\s*1fr/s);
 +    
expect(shellStyles).toMatch(/\.navigationChevron[^}]*transition:[^}]*transform/s);
 +    expect(shellStyles).toMatch(
 +      
/@media\s*\(prefers-reduced-motion:\s*reduce\)[\s\S]*\.navigationChildrenMotion[\s\S]*transition-duration:\s*0\.01ms/
 +    );
 +  });
++
+   it('marks the active destination with a line instead of a filled selection 
block', () => {
+     const activeRule = 
shellStyles.match(/\.navigationLinkActive\s*\{[^}]*\}/)?.[0] ?? '';
+ 
+     
expect(activeRule).toMatch(/border-left-color:\s*var\(--hb-brand-accent\)/);
+     expect(activeRule).toMatch(/background:\s*transparent/);
+     expect(activeRule).not.toMatch(/background:\s*var\(--hb-nav-selected\)/);
+   });
  });


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

Reply via email to