You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by ji...@apache.org on 2026/08/18 03:53:27 UTC

(ambari) branch frontend-refactor updated: AMBARI-26631: Complete React parity for the Alerts module (#4171)

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

JiaLiangC pushed a commit to branch frontend-refactor
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/frontend-refactor by this push:
     new 4b41f675c3 AMBARI-26631: Complete React parity for the Alerts module (#4171)
4b41f675c3 is described below

commit 4b41f675c3bdf8e1466bfd2bc320d3f083a0d487
Author: jialiang <ji...@gmail.com>
AuthorDate: Tue Aug 18 11:53:21 2026 +0800

    AMBARI-26631: Complete React parity for the Alerts module (#4171)
---
 ambari-web/latest/src/Utils/alertCreation.test.ts  |  95 +++
 ambari-web/latest/src/Utils/alertCreation.ts       | 153 ++++
 .../latest/src/Utils/alertDefinitions.test.ts      | 155 ++++
 ambari-web/latest/src/Utils/alertDefinitions.ts    | 253 ++++++
 ambari-web/latest/src/Utils/alertGroups.test.ts    | 198 +++++
 ambari-web/latest/src/Utils/alertGroups.ts         | 238 ++++++
 .../latest/src/Utils/alertNotifications.test.ts    | 154 ++++
 ambari-web/latest/src/Utils/alertNotifications.ts  | 222 +++++
 .../constants.ts => Utils/alertPolicy.test.ts}     |  45 +-
 .../Alerts/constants.ts => Utils/alertPolicy.ts}   |  44 +-
 ambari-web/latest/src/api/alertsApi.test.ts        |  61 ++
 ambari-web/latest/src/api/alertsApi.ts             | 319 ++------
 ambari-web/latest/src/components/EditableList.tsx  |  40 -
 ambari-web/latest/src/router/RoutesList.tsx        |  12 +
 .../latest/src/screens/Alerts/ActionsButton.tsx    | 910 +++++++--------------
 .../src/screens/Alerts/AlertConfigSection.tsx      | 154 ++--
 .../src/screens/Alerts/AlertDefinitionDetails.tsx  | 156 +++-
 .../src/screens/Alerts/AlertDefinitionWizard.tsx   | 320 ++++++++
 .../screens/Alerts/AlertInstancesTable.test.tsx    | 101 +++
 .../src/screens/Alerts/AlertInstancesTable.tsx     | 221 +++--
 ambari-web/latest/src/screens/Alerts/Alerts.tsx    | 112 +--
 .../latest/src/screens/Alerts/HeaderSection.tsx    |  47 +-
 .../src/screens/Alerts/InformationSection.tsx      |  88 +-
 ambari-web/latest/src/screens/Alerts/MenuBar.tsx   |  35 +-
 .../latest/src/screens/Alerts/alertUtils.test.ts   |  46 ++
 ambari-web/latest/src/screens/Alerts/alertUtils.ts |  28 +-
 .../screens/Alerts/components/AlertGroupModals.tsx |  49 +-
 .../Alerts/components/NotificationModals.tsx       |  52 +-
 ambari-web/latest/src/screens/Alerts/constants.ts  |   3 +-
 ambari-web/latest/src/screens/Alerts/types.ts      |  11 +-
 .../src/screens/Alerts/utils/alertGroupUtils.ts    |  45 +-
 docs/frontend-refactor/ember-baseline/05-alerts.md |  10 +-
 .../ember-baseline/15-five-pass-audit.md           |   4 +-
 .../ember-baseline/generated/feature-index.json    |   6 +-
 .../ember-baseline/generated/feature-index.md      |   6 +-
 .../react-current/05-alerts-gap.md                 | 240 ++++++
 .../react-current/issues/05-alerts.md              |  98 +++
 37 files changed, 3243 insertions(+), 1488 deletions(-)

diff --git a/ambari-web/latest/src/Utils/alertCreation.test.ts b/ambari-web/latest/src/Utils/alertCreation.test.ts
new file mode 100644
index 0000000000..f2ee8c4a03
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertCreation.test.ts
@@ -0,0 +1,95 @@
+/**
+ * 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 { describe, expect, it } from "vitest";
+import {
+  buildAlertCreationPayload,
+  INITIAL_ALERT_CREATION_FORM,
+  validateAlertCreation,
+  type AlertCreationForm,
+  type CreatableAlertType,
+} from "./alertCreation";
+
+const form = (type: CreatableAlertType): AlertCreationForm => ({
+  ...INITIAL_ALERT_CREATION_FORM,
+  type,
+  label: "Custom Health Check",
+  description: "Created in React",
+  serviceName: "HDFS",
+  componentName: "NAMENODE",
+  scope: "ANY",
+  interval: "2",
+  uri: "{{hdfs-site/dfs.namenode.http-address}}",
+  defaultPort: "9870",
+  connectionTimeout: "5",
+  scriptPath: "BIGTOP/3.2.0/services/HDFS/package/alerts/custom.py",
+  aggregateAlertName: "namenode_process",
+  warningThreshold: "10",
+  criticalThreshold: "20",
+  okText: "OK",
+  warningText: "Warning",
+  criticalText: "Critical",
+});
+
+describe("alert creation contracts", () => {
+  it.each(["PORT", "WEB", "SCRIPT", "AGGREGATE"] as CreatableAlertType[])("validates and builds %s", (type) => {
+    const value = form(type);
+    expect(validateAlertCreation(value)).toEqual([]);
+    const payload = buildAlertCreationPayload(value);
+    expect(payload).toMatchObject({
+      "AlertDefinition/name": "custom_health_check",
+      "AlertDefinition/label": "Custom Health Check",
+      "AlertDefinition/service_name": "HDFS",
+      "AlertDefinition/component_name": "NAMENODE",
+      "AlertDefinition/source": { type },
+    });
+  });
+
+  it("uses Classic-compatible Port source fields", () => {
+    expect(buildAlertCreationPayload(form("PORT"))["AlertDefinition/source"]).toEqual({
+      type: "PORT",
+      uri: "{{hdfs-site/dfs.namenode.http-address}}",
+      default_port: 9870,
+      reporting: {
+        ok: { text: "OK" },
+        warning: { text: "Warning", value: 10 },
+        critical: { text: "Critical", value: 20 },
+      },
+    });
+  });
+
+  it("repairs Script and Aggregate request paths beyond broken Classic wizard data", () => {
+    expect(buildAlertCreationPayload(form("SCRIPT"))["AlertDefinition/source"]).toMatchObject({
+      path: "BIGTOP/3.2.0/services/HDFS/package/alerts/custom.py",
+      parameters: [],
+    });
+    expect(buildAlertCreationPayload(form("AGGREGATE"))["AlertDefinition/source"]).toMatchObject({
+      alert_name: "namenode_process",
+      reporting: { units: "%", type: "PERCENT" },
+    });
+  });
+
+  it("rejects missing type-specific values and reversed thresholds", () => {
+    const value = form("PORT");
+    value.uri = "";
+    value.warningThreshold = "30";
+    value.criticalThreshold = "20";
+    expect(validateAlertCreation(value)).toContain("URI is required.");
+    expect(validateAlertCreation(value)).toContain("Warning Threshold cannot be greater than Critical Threshold.");
+  });
+});
diff --git a/ambari-web/latest/src/Utils/alertCreation.ts b/ambari-web/latest/src/Utils/alertCreation.ts
new file mode 100644
index 0000000000..de59655a48
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertCreation.ts
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type CreatableAlertType = "PORT" | "WEB" | "SCRIPT" | "AGGREGATE";
+
+export interface AlertCreationForm {
+  type: CreatableAlertType | "";
+  label: string;
+  description: string;
+  serviceName: string;
+  componentName: string;
+  scope: "ANY" | "HOST" | "SERVICE";
+  interval: string;
+  uri: string;
+  defaultPort: string;
+  connectionTimeout: string;
+  scriptPath: string;
+  aggregateAlertName: string;
+  warningThreshold: string;
+  criticalThreshold: string;
+  okText: string;
+  warningText: string;
+  criticalText: string;
+}
+
+export const INITIAL_ALERT_CREATION_FORM: AlertCreationForm = {
+  type: "",
+  label: "",
+  description: "",
+  serviceName: "",
+  componentName: "",
+  scope: "ANY",
+  interval: "1",
+  uri: "",
+  defaultPort: "",
+  connectionTimeout: "5",
+  scriptPath: "",
+  aggregateAlertName: "",
+  warningThreshold: "",
+  criticalThreshold: "",
+  okText: "",
+  warningText: "",
+  criticalText: "",
+};
+
+export function alertDefinitionName(label: string): string {
+  return label.trim().toLowerCase().replace(/\s+/g, "_");
+}
+
+export function validateAlertCreation(form: AlertCreationForm): string[] {
+  const errors: string[] = [];
+  if (!form.type) errors.push("Select an Alert Type.");
+  if (!form.label.trim()) errors.push("Alert Name is required.");
+  if (!form.serviceName) errors.push("Service is required.");
+  if (!Number.isInteger(Number(form.interval)) || Number(form.interval) < 1) errors.push("Check Interval must be a positive integer.");
+
+  if (form.type === "PORT" || form.type === "WEB") {
+    if (!form.uri.trim()) errors.push("URI is required.");
+    if (!/^\d+$/.test(form.defaultPort) || Number(form.defaultPort) < 1 || Number(form.defaultPort) > 65535) {
+      errors.push("Default Port must be between 1 and 65535.");
+    }
+  }
+  if (form.type === "WEB" && (!Number.isFinite(Number(form.connectionTimeout)) || Number(form.connectionTimeout) <= 0)) {
+    errors.push("Connection Timeout must be a positive number.");
+  }
+  if (form.type === "SCRIPT" && !form.scriptPath.trim()) errors.push("Script Path is required.");
+  if (form.type === "AGGREGATE" && !form.aggregateAlertName.trim()) errors.push("Referenced Alert Name is required.");
+
+  if (form.type === "PORT" || form.type === "AGGREGATE") {
+    const warning = Number(form.warningThreshold);
+    const critical = Number(form.criticalThreshold);
+    if (!Number.isFinite(warning) || warning <= 0) errors.push("Warning Threshold must be positive.");
+    if (!Number.isFinite(critical) || critical <= 0) errors.push("Critical Threshold must be positive.");
+    if (Number.isFinite(warning) && Number.isFinite(critical) && warning > critical) {
+      errors.push("Warning Threshold cannot be greater than Critical Threshold.");
+    }
+  }
+  return errors;
+}
+
+function reporting(form: AlertCreationForm, includeValues: boolean) {
+  const report: Record<string, unknown> = {
+    ok: { text: form.okText },
+    warning: includeValues
+      ? { text: form.warningText, value: Number(form.warningThreshold) }
+      : { text: form.warningText },
+    critical: includeValues
+      ? { text: form.criticalText, value: Number(form.criticalThreshold) }
+      : { text: form.criticalText },
+  };
+  if (form.type === "AGGREGATE") {
+    report.units = "%";
+    report.type = "PERCENT";
+  }
+  return report;
+}
+
+export function buildAlertCreationPayload(form: AlertCreationForm): Record<string, unknown> {
+  let source: Record<string, unknown>;
+  if (form.type === "PORT") {
+    source = {
+      type: "PORT",
+      uri: form.uri.trim(),
+      default_port: Number(form.defaultPort),
+      reporting: reporting(form, true),
+    };
+  } else if (form.type === "WEB") {
+    source = {
+      type: "WEB",
+      uri: {
+        http: form.uri.trim(),
+        default_port: Number(form.defaultPort),
+        connection_timeout: Number(form.connectionTimeout),
+      },
+      reporting: reporting(form, false),
+    };
+  } else if (form.type === "SCRIPT") {
+    source = { type: "SCRIPT", path: form.scriptPath.trim(), parameters: [] };
+  } else {
+    source = {
+      type: "AGGREGATE",
+      alert_name: form.aggregateAlertName.trim(),
+      reporting: reporting(form, true),
+    };
+  }
+
+  const payload: Record<string, unknown> = {
+    "AlertDefinition/name": alertDefinitionName(form.label),
+    "AlertDefinition/label": form.label.trim(),
+    "AlertDefinition/description": form.description,
+    "AlertDefinition/service_name": form.serviceName,
+    "AlertDefinition/scope": form.scope,
+    "AlertDefinition/interval": Number(form.interval),
+    "AlertDefinition/source": source,
+  };
+  if (form.componentName) payload["AlertDefinition/component_name"] = form.componentName;
+  return payload;
+}
diff --git a/ambari-web/latest/src/Utils/alertDefinitions.test.ts b/ambari-web/latest/src/Utils/alertDefinitions.test.ts
new file mode 100644
index 0000000000..7256e7806a
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertDefinitions.test.ts
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it, vi } from "vitest";
+import {
+  buildAlertDefinitionUpdate,
+  buildAlertDefinitionDetails,
+  countAlertHistoryByHost,
+  filterAndSortAlertInstances,
+  openAlertResponseInNewWindow,
+  validateAlertDefinitionConfiguration,
+  validateRepeatTolerance,
+} from "./alertDefinitions";
+
+describe("alert definition contracts", () => {
+  it("builds details directly even when a definition has no group membership", () => {
+    const details = buildAlertDefinitionDetails({
+      id: 42,
+      name: "service_check",
+      label: "Service Check",
+      description: "Checks the service",
+      enabled: true,
+      service_name: "HDFS",
+      component_name: "NAMENODE",
+      source: { type: "PORT" },
+    }, [], [{
+      definition_id: 42,
+      summary: {
+        WARNING: { count: 1, original_timestamp: "123", latest_text: "warning" },
+      },
+    }]);
+
+    expect(details).toMatchObject({
+      alert_definition_id: 42,
+      label: "Service Check",
+      groups: "",
+      source_type: "PORT",
+      state: "Enabled",
+    });
+    expect(details.statuses).toEqual([{
+      status: "warning",
+      count: 1,
+      maintenance_count: 0,
+      last_status_changed: "123",
+      latest_text: "warning",
+    }]);
+  });
+
+  it("counts 24-hour history records by host", () => {
+    expect(countAlertHistoryByHost([
+      { AlertHistory: { host_name: "host-1" } },
+      { AlertHistory: { host_name: "host-2" } },
+      { AlertHistory: { host_name: "host-1" } },
+      { AlertHistory: {} },
+    ])).toEqual({ "host-1": 2, "host-2": 1 });
+  });
+
+  it("reapplies instance filters and full-result sorting after refreshed data", () => {
+    const instances = [
+      { host_name: "host-1", service_name: "HDFS", state: "OK", last_updated_time: 30 },
+      { host_name: "host-2", service_name: "HDFS", state: "WARNING", last_updated_time: 20 },
+      { host_name: "host-3", service_name: "YARN", state: "OK", last_updated_time: 10 },
+    ];
+
+    expect(filterAndSortAlertInstances(
+      instances,
+      { service: "HDFS", hostName: "host", state: "" },
+      { id: "history_count", desc: true },
+      { "host-1": 1, "host-2": 4, "host-3": 10 },
+    ).map((instance) => instance.host_name)).toEqual(["host-2", "host-1"]);
+  });
+
+  it("accepts 1 through 99 and DEBUG repeat tolerance", () => {
+    expect(validateRepeatTolerance(1)).toBeNull();
+    expect(validateRepeatTolerance("99")).toBeNull();
+    expect(validateRepeatTolerance("DEBUG")).toBeNull();
+    expect(validateRepeatTolerance(0)).not.toBeNull();
+    expect(validateRepeatTolerance("1.5")).not.toBeNull();
+  });
+
+  it("validates interval, parameters, and threshold order", () => {
+    const errors = validateAlertDefinitionConfiguration({
+      interval: 0,
+      source: {
+        parameters: [{ name: "timeout", type: "NUMERIC", value: 0 }],
+        reporting: { warning: { value: 10 }, critical: { value: 5 } },
+      },
+    });
+    expect(errors).toEqual([
+      "Check Interval must be a positive integer.",
+      "timeout must be a positive number.",
+      "Warning threshold cannot be greater than Critical threshold.",
+    ]);
+  });
+
+  it("rejects non-positive and non-numeric reporting thresholds", () => {
+    expect(validateAlertDefinitionConfiguration({
+      interval: 1,
+      source: {
+        reporting: { warning: { value: 0 }, critical: { value: "invalid" } },
+      },
+    })).toEqual([
+      "Warning threshold must be positive.",
+      "Critical threshold must be a number.",
+    ]);
+  });
+
+  it("preserves description, interval, and the complete changed source", () => {
+    expect(buildAlertDefinitionUpdate({
+      description: "new",
+      interval: 5,
+      source: { type: "PORT", uri: "host", reporting: { critical: { value: 5 } } },
+    }, {
+      description: "old",
+      interval: 1,
+      source: { type: "PORT", uri: "host", reporting: { critical: { value: 2 } } },
+    })).toEqual({
+      "AlertDefinition/description": "new",
+      "AlertDefinition/interval": "5",
+      "AlertDefinition/source": { type: "PORT", uri: "host", reporting: { critical: { value: 5 } } },
+    });
+  });
+
+  it("opens response data through textContent", () => {
+    const pre = { textContent: "" };
+    const target = {
+      opener: {} as unknown,
+      document: {
+        title: "",
+        createElement: vi.fn(() => pre),
+        body: { replaceChildren: vi.fn() },
+      },
+    };
+    const openWindow = vi.fn(() => target);
+
+    expect(openAlertResponseInNewWindow("<script>unsafe()</script>", openWindow as any)).toBe(true);
+    expect(pre.textContent).toBe("<script>unsafe()</script>");
+    expect(target.document.body.replaceChildren).toHaveBeenCalledWith(pre);
+  });
+});
diff --git a/ambari-web/latest/src/Utils/alertDefinitions.ts b/ambari-web/latest/src/Utils/alertDefinitions.ts
new file mode 100644
index 0000000000..05580c532f
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertDefinitions.ts
@@ -0,0 +1,253 @@
+/**
+ * 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 { AlertStatus } from "../screens/Alerts/alertStatus";
+import type { MergedAlert } from "../screens/Alerts/types";
+
+interface AlertHistoryItem {
+  AlertHistory?: { host_name?: string };
+}
+
+interface SortableAlertInstance {
+  service_name?: string;
+  host_name?: string;
+  state?: string;
+  last_updated_time?: number;
+  [key: string]: unknown;
+}
+
+interface AlertInstanceFilters {
+  service: string;
+  hostName: string;
+  state: string;
+}
+
+interface AlertInstanceSort {
+  id: string;
+  desc: boolean;
+}
+
+interface DefinitionConfiguration {
+  description?: string;
+  interval?: number | string;
+  source?: {
+    parameters?: Array<{
+      name?: string;
+      type?: string;
+      value?: unknown;
+      visibility?: string;
+    }>;
+    reporting?: Record<string, unknown>;
+    [key: string]: unknown;
+  };
+}
+
+interface AlertDefinitionDetailsResource extends DefinitionConfiguration {
+  id: number;
+  cluster_name?: string;
+  enabled?: boolean;
+  name?: string;
+  label?: string;
+  service_name?: string;
+  component_name?: string;
+  source_type?: string;
+  repeat_tolerance?: string | number;
+  repeat_tolerance_enabled?: boolean;
+  help_url?: string;
+}
+
+interface AlertDefinitionGroupResource {
+  AlertGroup: {
+    id: number;
+    name: string;
+    default?: boolean;
+    cluster_name?: string;
+    definitions?: Array<number | { id?: number }>;
+  };
+}
+
+interface GroupedSummaryResource {
+  definition_id: string | number;
+  summary?: Record<string, {
+    count?: number;
+    maintenance_count?: number;
+    original_timestamp?: string | null;
+    latest_text?: string;
+  }>;
+}
+
+export function countAlertHistoryByHost(items: AlertHistoryItem[] = []): Record<string, number> {
+  return items.reduce<Record<string, number>>((counts, item) => {
+    const hostName = item.AlertHistory?.host_name;
+    if (hostName) counts[hostName] = (counts[hostName] || 0) + 1;
+    return counts;
+  }, {});
+}
+
+export function filterAndSortAlertInstances<T extends SortableAlertInstance>(
+  instances: T[],
+  filters: AlertInstanceFilters,
+  sorting: AlertInstanceSort | undefined,
+  historyCounts: Record<string, number>,
+): T[] {
+  const filtered = instances.filter((instance) => {
+    if (filters.service && filters.service !== "All" && instance.service_name !== filters.service) return false;
+    if (filters.hostName && !String(instance.host_name || "").toLowerCase().includes(filters.hostName.toLowerCase())) return false;
+    if (filters.state && filters.state !== "All" && String(instance.state || "").toLowerCase() !== filters.state.toLowerCase()) return false;
+    return true;
+  });
+
+  if (!sorting) return filtered;
+
+  return filtered.sort((left, right) => {
+    let comparison: number;
+    if (sorting.id === "history_count") {
+      comparison = (historyCounts[left.host_name || ""] || 0) - (historyCounts[right.host_name || ""] || 0);
+    } else if (sorting.id === "last_updated_time") {
+      comparison = Number(left.last_updated_time || 0) - Number(right.last_updated_time || 0);
+    } else {
+      comparison = String(left[sorting.id] ?? "").localeCompare(String(right[sorting.id] ?? ""));
+    }
+    return sorting.desc ? -comparison : comparison;
+  });
+}
+
+export function buildAlertDefinitionDetails(
+  definition: AlertDefinitionDetailsResource,
+  groups: AlertDefinitionGroupResource[] = [],
+  summaries: GroupedSummaryResource[] = [],
+): MergedAlert {
+  const memberships = groups.filter((group) => (group.AlertGroup.definitions || []).some((reference) =>
+    Number(typeof reference === "number" ? reference : reference.id) === definition.id,
+  ));
+  const summary = summaries.find((item) => Number(item.definition_id) === definition.id)?.summary || {};
+  const priority: Record<string, number> = { critical: 4, warning: 3, ok: 2, unknown: 1 };
+  const statuses = Object.entries(summary)
+    .filter(([, value]) => (value.count || 0) > 0 || (value.maintenance_count || 0) > 0)
+    .map(([status, value]) => ({
+      status: status.toLowerCase() as AlertStatus,
+      count: value.count || 0,
+      maintenance_count: value.maintenance_count || 0,
+      last_status_changed: value.original_timestamp || null,
+      latest_text: value.latest_text || "",
+    }))
+    .sort((left, right) => (priority[right.status] || 0) - (priority[left.status] || 0));
+  const primaryGroup = memberships[0]?.AlertGroup;
+  const lastStatusChanged = statuses[0]?.last_status_changed || "Unknown";
+
+  return {
+    cluster_name: definition.cluster_name || primaryGroup?.cluster_name || "",
+    alert_group_id: primaryGroup?.id || 0,
+    alert_group_name: primaryGroup?.name || "",
+    enabled: Boolean(definition.enabled),
+    name: definition.name || "",
+    label: definition.label || definition.name || "",
+    description: definition.description || "",
+    serviceDisplayName: definition.service_name || "",
+    component_name: definition.component_name || "",
+    alert_definition_id: definition.id,
+    source_type: String(definition.source?.type || definition.source_type || ""),
+    repeat_tolerance: definition.repeat_tolerance ?? 1,
+    repeat_tolerance_enabled: Boolean(definition.repeat_tolerance_enabled),
+    help_url: definition.help_url,
+    statuses,
+    last_status_changed: lastStatusChanged,
+    lastTriggeredFormatted: lastStatusChanged,
+    lastTriggeredAgoFormatted: "Unknown",
+    lastTriggeredRaw: lastStatusChanged === "Unknown" ? null : lastStatusChanged,
+    state: definition.enabled ? "Enabled" : "Disabled",
+    latest_text: statuses[0]?.latest_text || "",
+    groups: memberships.map((group) =>
+      `${group.AlertGroup.name}${group.AlertGroup.default ? " Default" : ""}`,
+    ).join(", "),
+  };
+}
+
+export function validateRepeatTolerance(value: string | number): string | null {
+  const normalized = String(value).trim();
+  if (normalized === "DEBUG") return null;
+  if (!/^\d+$/.test(normalized) || Number(normalized) < 1 || Number(normalized) > 99) {
+    return "Check Count must be an integer between 1 and 99, or DEBUG.";
+  }
+  return null;
+}
+
+function reportingValue(source: DefinitionConfiguration["source"], state: string): number | null {
+  const entry = source?.reporting?.[state];
+  if (!entry || typeof entry !== "object" || !("value" in entry)) return null;
+  const value = Number((entry as { value?: unknown }).value);
+  return Number.isFinite(value) ? value : null;
+}
+
+function hasReportingValue(source: DefinitionConfiguration["source"], state: string): boolean {
+  const entry = source?.reporting?.[state];
+  return Boolean(entry && typeof entry === "object" && "value" in entry);
+}
+
+export function validateAlertDefinitionConfiguration(configuration: DefinitionConfiguration): string[] {
+  const errors: string[] = [];
+  const interval = Number(configuration.interval);
+  if (!Number.isInteger(interval) || interval < 1) errors.push("Check Interval must be a positive integer.");
+
+  for (const parameter of configuration.source?.parameters || []) {
+    if (parameter.visibility === "HIDDEN" || parameter.visibility === "READ_ONLY") continue;
+    const value = String(parameter.value ?? "").trim();
+    if (!value) errors.push(`${parameter.name || "Parameter"} is required.`);
+    if ((parameter.type === "NUMERIC" || parameter.type === "PERCENT") && (!Number.isFinite(Number(value)) || Number(value) <= 0)) {
+      errors.push(`${parameter.name || "Parameter"} must be a positive number.`);
+    }
+  }
+
+  const warning = reportingValue(configuration.source, "warning");
+  const critical = reportingValue(configuration.source, "critical");
+  if (hasReportingValue(configuration.source, "warning") && warning === null) errors.push("Warning threshold must be a number.");
+  else if (warning !== null && warning <= 0) errors.push("Warning threshold must be positive.");
+  if (hasReportingValue(configuration.source, "critical") && critical === null) errors.push("Critical threshold must be a number.");
+  else if (critical !== null && critical <= 0) errors.push("Critical threshold must be positive.");
+  if (warning !== null && critical !== null && warning > critical) {
+    errors.push("Warning threshold cannot be greater than Critical threshold.");
+  }
+  return errors;
+}
+
+export function buildAlertDefinitionUpdate(
+  current: DefinitionConfiguration,
+  original: DefinitionConfiguration,
+): Record<string, unknown> {
+  const payload: Record<string, unknown> = {};
+  if (current.description !== original.description) payload["AlertDefinition/description"] = current.description || "";
+  if (String(current.interval) !== String(original.interval)) payload["AlertDefinition/interval"] = String(current.interval);
+  if (JSON.stringify(current.source || {}) !== JSON.stringify(original.source || {})) {
+    payload["AlertDefinition/source"] = JSON.parse(JSON.stringify(current.source || {}));
+  }
+  return payload;
+}
+
+export function openAlertResponseInNewWindow(
+  value: string,
+  openWindow: typeof window.open = window.open.bind(window),
+): boolean {
+  const target = openWindow("about:blank", "_blank");
+  if (!target) return false;
+  target.opener = null;
+  target.document.title = "Ambari Alert Response";
+  const pre = target.document.createElement("pre");
+  pre.textContent = value;
+  target.document.body.replaceChildren(pre);
+  return true;
+}
diff --git a/ambari-web/latest/src/Utils/alertGroups.test.ts b/ambari-web/latest/src/Utils/alertGroups.test.ts
new file mode 100644
index 0000000000..bd723f8a3c
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertGroups.test.ts
@@ -0,0 +1,198 @@
+/**
+ * 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 { describe, expect, it, vi } from "vitest";
+import {
+  buildAlertGroupPayload,
+  executeAlertGroupSave,
+  planAlertGroupSave,
+  reconcileAlertGroupSave,
+  reconcileAlertGroupSaveWithServer,
+  validateAlertGroupName,
+} from "./alertGroups";
+import type { AlertGroupItem } from "../screens/Alerts/types";
+
+const group = (
+  id: number,
+  flags: Partial<AlertGroupItem["AlertGroup"]> = {},
+): AlertGroupItem => ({
+  AlertGroup: {
+    id,
+    name: `group-${id}`,
+    cluster_name: "c1",
+    default: false,
+    definitions: [],
+    targets: [],
+    ...flags,
+  },
+});
+
+describe("alert group contracts", () => {
+  it("applies the Classic Alert Group name rules", () => {
+    expect(validateAlertGroupName("  operators_1  ")).toBeNull();
+    expect(validateAlertGroupName("   ")).toBe("Group name cannot be empty");
+    expect(validateAlertGroupName("operators/group")).toBe("Group name contains invalid characters");
+  });
+
+  it("builds a complete replacement payload with numeric definitions and targets", () => {
+    expect(buildAlertGroupPayload(group(4, {
+      name: "  operators  ",
+      definitions: [1, { id: 2 }, { id: -1 }],
+      targets: [{ id: 7, name: "mail", notification_type: "EMAIL" }, { id: 8, name: "snmp", notification_type: "AMBARI_SNMP" }],
+    }))).toEqual({
+      AlertGroup: {
+        name: "operators",
+        definitions: [1, 2],
+        targets: [7, 8],
+      },
+    });
+  });
+
+  it("classifies only server mutations", () => {
+    const plan = planAlertGroupSave([
+      group(-1, { _isNew: true }),
+      group(2, { _isModified: true }),
+      group(3, { _deleted: true }),
+      group(-4, { _deleted: true }),
+      group(5),
+    ]);
+
+    expect(plan.create.map((item) => item.AlertGroup.id)).toEqual([-1]);
+    expect(plan.update.map((item) => item.AlertGroup.id)).toEqual([2]);
+    expect(plan.delete.map((item) => item.AlertGroup.id)).toEqual([3]);
+  });
+
+  it("settles every delete before starting concurrent updates and creates", async () => {
+    let releaseDeletes!: () => void;
+    const deleteBarrier = new Promise<void>((resolve) => { releaseDeletes = resolve; });
+    const calls: string[] = [];
+    const operations = {
+      delete: vi.fn(async (item: AlertGroupItem) => {
+        calls.push(`delete-${item.AlertGroup.id}`);
+        await deleteBarrier;
+      }),
+      update: vi.fn(async (item: AlertGroupItem) => { calls.push(`update-${item.AlertGroup.id}`); }),
+      create: vi.fn(async (item: AlertGroupItem) => { calls.push(`create-${item.AlertGroup.id}`); }),
+    };
+
+    const resultPromise = executeAlertGroupSave({
+      delete: [group(1), group(2)],
+      update: [group(3)],
+      create: [group(-4)],
+    }, operations);
+    await Promise.resolve();
+    expect(calls).toEqual(["delete-1", "delete-2"]);
+
+    releaseDeletes();
+    const result = await resultPromise;
+    expect(calls.slice(2).sort()).toEqual(["create--4", "update-3"]);
+    expect(result).toEqual({
+      successful: { create: 1, update: 1, delete: 2 },
+      failures: [],
+    });
+  });
+
+  it("continues the second phase and aggregates rejected operations", async () => {
+    const failure = new Error("rejected");
+    const result = await executeAlertGroupSave({
+      delete: [group(1)],
+      update: [group(2)],
+      create: [group(-3)],
+    }, {
+      delete: vi.fn().mockRejectedValue(failure),
+      update: vi.fn().mockResolvedValue(undefined),
+      create: vi.fn().mockRejectedValue(failure),
+    });
+
+    expect(result.successful).toEqual({ create: 0, update: 1, delete: 0 });
+    expect(result.failures.map(({ kind, groupId }) => ({ kind, groupId }))).toEqual([
+      { kind: "delete", groupId: 1 },
+      { kind: "create", groupId: -3 },
+    ]);
+  });
+
+  it("retains only failed pending operations when a server refresh is unavailable", () => {
+    const reconciled = reconcileAlertGroupSave([
+      group(-1, { _isNew: true }),
+      group(-2, { _isNew: true }),
+      group(3, { _isModified: true }),
+      group(4, { _deleted: true }),
+    ], [
+      { kind: "create", groupId: -2, groupName: "group--2", reason: new Error("failed") },
+      { kind: "delete", groupId: 4, groupName: "group-4", reason: new Error("failed") },
+    ]);
+
+    expect(reconciled.map((item) => item.AlertGroup.id)).toEqual([-2, 3, 4]);
+    expect(reconciled.find((item) => item.AlertGroup.id === 3)?.AlertGroup._isModified).toBe(false);
+  });
+
+  it("uses refreshed server state to resolve ambiguous request failures", () => {
+    const failedCreate = group(-1, {
+      name: "created",
+      definitions: [{ id: 10 }],
+      targets: [{ id: 20, name: "mail", notification_type: "EMAIL" }],
+      _isNew: true,
+    });
+    const failedUpdate = group(2, {
+      name: "updated",
+      definitions: [{ id: 11 }],
+      targets: [],
+      _isModified: true,
+    });
+    const failedDelete = group(3, { _deleted: true });
+    const failures = [
+      { kind: "create" as const, groupId: -1, groupName: "created", reason: new Error("timeout") },
+      { kind: "update" as const, groupId: 2, groupName: "updated", reason: new Error("timeout") },
+      { kind: "delete" as const, groupId: 3, groupName: "group-3", reason: new Error("timeout") },
+    ];
+
+    const reconciled = reconcileAlertGroupSaveWithServer([
+      group(7, {
+        name: "created",
+        definitions: [{ id: 10 }],
+        targets: [{ id: 20, name: "mail", notification_type: "EMAIL" }],
+      }),
+      group(2, { name: "updated", definitions: [{ id: 11 }], targets: [] }),
+    ], [failedCreate, failedUpdate, failedDelete], failures);
+
+    expect(reconciled.failures).toEqual([]);
+    expect(reconciled.groups.map((item) => item.AlertGroup.id)).toEqual([7, 2]);
+    expect(reconciled.groups.some((item) => item.AlertGroup._isNew || item.AlertGroup._deleted)).toBe(false);
+  });
+
+  it("keeps only failures that refreshed server state proves are still pending", () => {
+    const failedCreate = group(-1, { name: "created", definitions: [{ id: 10 }], _isNew: true });
+    const failedUpdate = group(2, { name: "updated", definitions: [{ id: 11 }], _isModified: true });
+    const failedDelete = group(3, { _deleted: true });
+    const failures = [
+      { kind: "create" as const, groupId: -1, groupName: "created", reason: new Error("failed") },
+      { kind: "update" as const, groupId: 2, groupName: "updated", reason: new Error("failed") },
+      { kind: "delete" as const, groupId: 3, groupName: "group-3", reason: new Error("failed") },
+    ];
+
+    const reconciled = reconcileAlertGroupSaveWithServer([
+      group(2, { name: "old", definitions: [] }),
+      group(3),
+    ], [failedCreate, failedUpdate, failedDelete], failures);
+
+    expect(reconciled.failures).toEqual(failures);
+    expect(reconciled.groups.map((item) => item.AlertGroup.id)).toEqual([2, 3, -1]);
+    expect(reconciled.groups[0].AlertGroup._isModified).toBe(true);
+    expect(reconciled.groups[1].AlertGroup._deleted).toBe(true);
+  });
+});
diff --git a/ambari-web/latest/src/Utils/alertGroups.ts b/ambari-web/latest/src/Utils/alertGroups.ts
new file mode 100644
index 0000000000..a27055e723
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertGroups.ts
@@ -0,0 +1,238 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type {
+  AlertDefinitionReference,
+  AlertGroupItem,
+  AlertTarget,
+} from "../screens/Alerts/types";
+
+export interface AlertGroupPayload {
+  AlertGroup: {
+    name: string;
+    definitions: number[];
+    targets: number[];
+  };
+}
+
+export interface AlertGroupSavePlan {
+  create: AlertGroupItem[];
+  update: AlertGroupItem[];
+  delete: AlertGroupItem[];
+}
+
+export type AlertGroupOperationKind = "create" | "update" | "delete";
+
+export interface AlertGroupSaveFailure {
+  kind: AlertGroupOperationKind;
+  groupId: number;
+  groupName: string;
+  reason: unknown;
+}
+
+export interface AlertGroupSaveResult {
+  successful: Record<AlertGroupOperationKind, number>;
+  failures: AlertGroupSaveFailure[];
+}
+
+export interface AlertGroupServerReconciliation {
+  groups: AlertGroupItem[];
+  failures: AlertGroupSaveFailure[];
+}
+
+export interface AlertGroupSaveOperations {
+  create: (group: AlertGroupItem, payload: AlertGroupPayload) => Promise<unknown>;
+  update: (group: AlertGroupItem, payload: AlertGroupPayload) => Promise<unknown>;
+  delete: (group: AlertGroupItem) => Promise<unknown>;
+}
+
+export function validateAlertGroupName(name: string): string | null {
+  const normalized = name.trim();
+  if (!normalized) return "Group name cannot be empty";
+  if (!/^[\s0-9a-z_-]+$/i.test(normalized)) {
+    return "Group name contains invalid characters";
+  }
+  return null;
+}
+
+function numericIds(values: Array<AlertDefinitionReference | AlertTarget> | undefined): number[] {
+  if (!values) {
+    return [];
+  }
+
+  return values.flatMap((value) => {
+    const candidate = typeof value === "number" ? value : value?.id;
+    const id = Number(candidate);
+    return Number.isFinite(id) && id > 0 ? [id] : [];
+  });
+}
+
+export function buildAlertGroupPayload(group: AlertGroupItem): AlertGroupPayload {
+  return {
+    AlertGroup: {
+      name: group.AlertGroup.name.trim(),
+      definitions: numericIds(group.AlertGroup.definitions),
+      targets: numericIds(group.AlertGroup.targets),
+    },
+  };
+}
+
+export function planAlertGroupSave(groups: AlertGroupItem[]): AlertGroupSavePlan {
+  return groups.reduce<AlertGroupSavePlan>((plan, group) => {
+    if (group.AlertGroup._deleted) {
+      if (group.AlertGroup.id > 0) {
+        plan.delete.push(group);
+      }
+    } else if (group.AlertGroup._isNew) {
+      plan.create.push(group);
+    } else if (group.AlertGroup._isModified && group.AlertGroup.id > 0) {
+      plan.update.push(group);
+    }
+    return plan;
+  }, { create: [], update: [], delete: [] });
+}
+
+export async function executeAlertGroupSave(
+  plan: AlertGroupSavePlan,
+  operations: AlertGroupSaveOperations,
+): Promise<AlertGroupSaveResult> {
+  const successful: AlertGroupSaveResult["successful"] = {
+    create: 0,
+    update: 0,
+    delete: 0,
+  };
+  const failures: AlertGroupSaveFailure[] = [];
+
+  const settle = async (
+    kind: AlertGroupOperationKind,
+    groups: AlertGroupItem[],
+    operation: (group: AlertGroupItem) => Promise<unknown>,
+  ) => {
+    const results = await Promise.allSettled(groups.map(operation));
+    results.forEach((result, index) => {
+      const group = groups[index];
+      if (result.status === "fulfilled") {
+        successful[kind] += 1;
+      } else {
+        failures.push({
+          kind,
+          groupId: group.AlertGroup.id,
+          groupName: group.AlertGroup.name,
+          reason: result.reason,
+        });
+      }
+    });
+  };
+
+  await settle("delete", plan.delete, operations.delete);
+
+  await Promise.all([
+    settle("update", plan.update, (group) => operations.update(group, buildAlertGroupPayload(group))),
+    settle("create", plan.create, (group) => operations.create(group, buildAlertGroupPayload(group))),
+  ]);
+
+  return { successful, failures };
+}
+
+export function reconcileAlertGroupSave(
+  groups: AlertGroupItem[],
+  failures: AlertGroupSaveFailure[],
+): AlertGroupItem[] {
+  const failed = new Set(failures.map((failure) => `${failure.kind}:${failure.groupId}`));
+  return groups.flatMap((group) => {
+    const id = group.AlertGroup.id;
+    if (group.AlertGroup._isNew) {
+      return failed.has(`create:${id}`) ? [group] : [];
+    }
+    if (group.AlertGroup._deleted) {
+      return failed.has(`delete:${id}`) ? [group] : [];
+    }
+    if (group.AlertGroup._isModified && !failed.has(`update:${id}`)) {
+      return [{
+        ...group,
+        AlertGroup: { ...group.AlertGroup, _isModified: false },
+      }];
+    }
+    return [group];
+  });
+}
+
+function sameIds(left: number[], right: number[]): boolean {
+  if (left.length !== right.length) return false;
+  const sortedLeft = [...left].sort((a, b) => a - b);
+  const sortedRight = [...right].sort((a, b) => a - b);
+  return sortedLeft.every((id, index) => id === sortedRight[index]);
+}
+
+function hasDesiredState(serverGroup: AlertGroupItem, localGroup: AlertGroupItem): boolean {
+  const server = buildAlertGroupPayload(serverGroup).AlertGroup;
+  const local = buildAlertGroupPayload(localGroup).AlertGroup;
+  return server.name === local.name &&
+    sameIds(server.definitions, local.definitions) &&
+    sameIds(server.targets, local.targets);
+}
+
+export function reconcileAlertGroupSaveWithServer(
+  serverGroups: AlertGroupItem[],
+  localGroups: AlertGroupItem[],
+  failures: AlertGroupSaveFailure[],
+): AlertGroupServerReconciliation {
+  const unresolved = new Set(failures);
+  const failedUpdates = new Map<number, { failure: AlertGroupSaveFailure; group?: AlertGroupItem }>();
+  const failedDeletes = new Map<number, { failure: AlertGroupSaveFailure; group?: AlertGroupItem }>();
+
+  failures.forEach((failure) => {
+    const group = localGroups.find((candidate) => candidate.AlertGroup.id === failure.groupId);
+    if (failure.kind === "update") failedUpdates.set(failure.groupId, { failure, group });
+    if (failure.kind === "delete") failedDeletes.set(failure.groupId, { failure, group });
+  });
+
+  const groups = serverGroups.map((serverGroup) => {
+    const id = serverGroup.AlertGroup.id;
+    const failedUpdate = failedUpdates.get(id);
+    if (failedUpdate?.group) {
+      if (hasDesiredState(serverGroup, failedUpdate.group)) {
+        unresolved.delete(failedUpdate.failure);
+        return serverGroup;
+      }
+      return failedUpdate.group;
+    }
+
+    const failedDelete = failedDeletes.get(id);
+    if (failedDelete?.group) return failedDelete.group;
+    return serverGroup;
+  });
+
+  failedDeletes.forEach(({ failure }) => {
+    if (!serverGroups.some((group) => group.AlertGroup.id === failure.groupId)) {
+      unresolved.delete(failure);
+    }
+  });
+
+  failures.filter((failure) => failure.kind === "create").forEach((failure) => {
+    const localGroup = localGroups.find((group) => group.AlertGroup.id === failure.groupId);
+    if (!localGroup) return;
+    if (serverGroups.some((serverGroup) => hasDesiredState(serverGroup, localGroup))) {
+      unresolved.delete(failure);
+    } else {
+      groups.push(localGroup);
+    }
+  });
+
+  return { groups, failures: [...unresolved] };
+}
diff --git a/ambari-web/latest/src/Utils/alertNotifications.test.ts b/ambari-web/latest/src/Utils/alertNotifications.test.ts
new file mode 100644
index 0000000000..ddd7356549
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertNotifications.test.ts
@@ -0,0 +1,154 @@
+/**
+ * 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 { describe, expect, it } from "vitest";
+import {
+  buildAlertNotificationPayload,
+  notificationTypeToApi,
+  notificationTypeToUi,
+  validateAlertNotificationForm,
+  type AlertNotificationForm,
+} from "./alertNotifications";
+
+const base = (type: AlertNotificationForm["type"]): AlertNotificationForm => ({
+  name: "target",
+  description: "description",
+  type,
+  global: false,
+  groups: [1, 2],
+  alertStates: ["CRITICAL", "WARNING"],
+});
+
+describe("alert notification contracts", () => {
+  it("maps API and UI notification types in both directions", () => {
+    expect(notificationTypeToApi("SNMP")).toBe("AMBARI_SNMP");
+    expect(notificationTypeToApi("Custom SNMP")).toBe("SNMP");
+    expect(notificationTypeToApi("Alert Script")).toBe("ALERT_SCRIPT");
+    expect(notificationTypeToUi("AMBARI_SNMP")).toBe("SNMP");
+    expect(notificationTypeToUi("SNMP")).toBe("Custom SNMP");
+    expect(notificationTypeToUi("ALERT_SCRIPT")).toBe("Alert Script");
+  });
+
+  it("builds authenticated Email and preserves an unchanged sensitive password", () => {
+    const payload = buildAlertNotificationPayload({
+      ...base("EMAIL"),
+      recipients: "one@example.test, two@example.test",
+      smtpHost: "smtp.example.test",
+      smtpPort: "587",
+      emailFrom: "ambari@example.test",
+      useAuthentication: true,
+      username: "ambari",
+      password: "",
+      passwordConfirmation: "",
+      startTls: true,
+      preserveSensitivePassword: true,
+      existingProperties: { "ambari.dispatch.credential.password": "********" },
+    });
+
+    expect(payload.AlertTarget.notification_type).toBe("EMAIL");
+    expect(payload.AlertTarget.properties).toMatchObject({
+      "ambari.dispatch.recipients": ["one@example.test", "two@example.test"],
+      "mail.smtp.auth": true,
+      "ambari.dispatch.credential.username": "ambari",
+      "ambari.dispatch.credential.password": "********",
+      "mail.smtp.starttls.enable": true,
+    });
+  });
+
+  it("builds built-in SNMP with recipients instead of an unsupported host key", () => {
+    const payload = buildAlertNotificationPayload({
+      ...base("SNMP"),
+      snmpVersion: "SNMPv2c",
+      snmpCommunity: "public",
+      snmpHosts: "host-1,host-2",
+      snmpPort: "162",
+    });
+
+    expect(payload.AlertTarget.notification_type).toBe("AMBARI_SNMP");
+    expect(payload.AlertTarget.properties["ambari.dispatch.recipients"]).toEqual(["host-1", "host-2"]);
+    expect(payload.AlertTarget.properties).not.toHaveProperty("ambari.dispatch.snmp.host");
+  });
+
+  it("builds Custom SNMP with all three OID keys", () => {
+    const payload = buildAlertNotificationPayload({
+      ...base("Custom SNMP"),
+      snmpCommunity: "private",
+      snmpHosts: "host-1",
+      snmpPort: "162",
+      snmpOid: "1.3.6.1.4.1",
+    });
+
+    expect(payload.AlertTarget.notification_type).toBe("SNMP");
+    expect(payload.AlertTarget.properties).toMatchObject({
+      "ambari.dispatch.snmp.oids.trap": "1.3.6.1.4.1",
+      "ambari.dispatch.snmp.oids.subject": "1.3.6.1.4.1",
+      "ambari.dispatch.snmp.oids.body": "1.3.6.1.4.1",
+    });
+  });
+
+  it("builds Alert Script with the Classic property names", () => {
+    const payload = buildAlertNotificationPayload({
+      ...base("Alert Script"),
+      scriptDispatchProperty: "scripts",
+      scriptFilename: "notify.py",
+    });
+
+    expect(payload.AlertTarget.notification_type).toBe("ALERT_SCRIPT");
+    expect(payload.AlertTarget.properties).toEqual({
+      "ambari.dispatch-property.script": "scripts",
+      "ambari.dispatch-property.script.filename": "notify.py",
+    });
+  });
+
+  it("rejects custom property conflicts with corrected built-in keys", () => {
+    const errors = validateAlertNotificationForm({
+      ...base("Custom SNMP"),
+      snmpCommunity: "private",
+      snmpHosts: "host-1",
+      snmpPort: "162",
+      snmpOid: "1.3.6",
+      customProperties: [
+        { name: "ambari.dispatch.snmp.oids.body", value: "duplicate" },
+        { name: "invalid key", value: "bad" },
+      ],
+    });
+
+    expect(errors).toContain("Custom property conflicts with a built-in property: ambari.dispatch.snmp.oids.body.");
+    expect(errors).toContain("Invalid custom property name: invalid key.");
+  });
+
+  it("validates addresses, names, and new authenticated passwords", () => {
+    const errors = validateAlertNotificationForm({
+      ...base("EMAIL"),
+      name: "invalid/name",
+      recipients: "not-an-address",
+      smtpHost: "smtp.example.test",
+      smtpPort: "587",
+      emailFrom: "also-invalid",
+      useAuthentication: true,
+      username: "ambari",
+      password: "",
+      passwordConfirmation: "different",
+    });
+
+    expect(errors).toContain("Notification name contains invalid characters.");
+    expect(errors).toContain("Enter valid Email recipients.");
+    expect(errors).toContain("Email From must be a valid address.");
+    expect(errors).toContain("SMTP Password is required when authentication is enabled.");
+  });
+});
diff --git a/ambari-web/latest/src/Utils/alertNotifications.ts b/ambari-web/latest/src/Utils/alertNotifications.ts
new file mode 100644
index 0000000000..d945308e2d
--- /dev/null
+++ b/ambari-web/latest/src/Utils/alertNotifications.ts
@@ -0,0 +1,222 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type AlertNotificationUiType = "EMAIL" | "SNMP" | "Custom SNMP" | "Alert Script";
+export type AlertNotificationApiType = "EMAIL" | "AMBARI_SNMP" | "SNMP" | "ALERT_SCRIPT";
+
+const PASSWORD_KEY = "ambari.dispatch.credential.password";
+
+const BUILT_IN_KEYS: Record<AlertNotificationUiType, string[]> = {
+  EMAIL: [
+    "ambari.dispatch.recipients",
+    "mail.smtp.host",
+    "mail.smtp.port",
+    "mail.smtp.from",
+    "mail.smtp.auth",
+    "ambari.dispatch.credential.username",
+    PASSWORD_KEY,
+    "mail.smtp.starttls.enable",
+  ],
+  SNMP: [
+    "ambari.dispatch.snmp.version",
+    "ambari.dispatch.snmp.community",
+    "ambari.dispatch.recipients",
+    "ambari.dispatch.snmp.port",
+  ],
+  "Custom SNMP": [
+    "ambari.dispatch.snmp.version",
+    "ambari.dispatch.snmp.community",
+    "ambari.dispatch.recipients",
+    "ambari.dispatch.snmp.port",
+    "ambari.dispatch.snmp.oids.trap",
+    "ambari.dispatch.snmp.oids.subject",
+    "ambari.dispatch.snmp.oids.body",
+  ],
+  "Alert Script": [
+    "ambari.dispatch-property.script",
+    "ambari.dispatch-property.script.filename",
+  ],
+};
+
+export interface AlertNotificationForm {
+  name: string;
+  description: string;
+  type: AlertNotificationUiType;
+  global: boolean;
+  groups: number[];
+  alertStates: string[];
+  recipients?: string;
+  smtpHost?: string;
+  smtpPort?: string;
+  emailFrom?: string;
+  useAuthentication?: boolean;
+  username?: string;
+  password?: string;
+  passwordConfirmation?: string;
+  startTls?: boolean;
+  snmpVersion?: string;
+  snmpCommunity?: string;
+  snmpHosts?: string;
+  snmpPort?: string;
+  snmpOid?: string;
+  scriptDispatchProperty?: string;
+  scriptFilename?: string;
+  customProperties?: Array<{ name: string; value: string }>;
+  existingProperties?: Record<string, unknown>;
+  preserveSensitivePassword?: boolean;
+}
+
+export interface AlertNotificationPayload {
+  AlertTarget: {
+    name: string;
+    description: string;
+    global: boolean;
+    notification_type: AlertNotificationApiType;
+    alert_states: string[];
+    properties: Record<string, unknown>;
+    groups?: number[];
+  };
+}
+
+export function notificationTypeToApi(type: AlertNotificationUiType): AlertNotificationApiType {
+  if (type === "SNMP") return "AMBARI_SNMP";
+  if (type === "Custom SNMP") return "SNMP";
+  if (type === "Alert Script") return "ALERT_SCRIPT";
+  return "EMAIL";
+}
+
+export function notificationTypeToUi(type: string): AlertNotificationUiType {
+  if (type === "AMBARI_SNMP") return "SNMP";
+  if (type === "SNMP") return "Custom SNMP";
+  if (type === "ALERT_SCRIPT") return "Alert Script";
+  return "EMAIL";
+}
+
+export function notificationBuiltInKeys(type: AlertNotificationUiType): string[] {
+  return [...BUILT_IN_KEYS[type]];
+}
+
+function list(value = ""): string[] {
+  return value.split(",").map((item) => item.trim()).filter(Boolean);
+}
+
+function isEmail(value: string): boolean {
+  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
+}
+
+export function validateAlertNotificationForm(form: AlertNotificationForm): string[] {
+  const errors: string[] = [];
+  if (!form.name.trim()) errors.push("Notification name is required.");
+  else if (!/^[\s0-9a-z_-]+$/i.test(form.name)) errors.push("Notification name contains invalid characters.");
+  if (!form.global && form.groups.length === 0) errors.push("Select at least one Alert Group.");
+  if (form.alertStates.length === 0) errors.push("Select at least one severity.");
+
+  if (form.type === "EMAIL") {
+    const recipients = list(form.recipients);
+    if (recipients.length === 0) errors.push("At least one Email recipient is required.");
+    else if (recipients.some((recipient) => !isEmail(recipient))) errors.push("Enter valid Email recipients.");
+    if (!form.smtpHost?.trim()) errors.push("SMTP Server is required.");
+    if (!/^\d+$/.test(form.smtpPort?.trim() || "") || Number(form.smtpPort) < 1 || Number(form.smtpPort) > 65535) {
+      errors.push("SMTP Port must be between 1 and 65535.");
+    }
+    if (!form.emailFrom?.trim()) errors.push("Email From is required.");
+    else if (!isEmail(form.emailFrom.trim())) errors.push("Email From must be a valid address.");
+    if (form.useAuthentication && !form.username?.trim()) errors.push("SMTP Username is required when authentication is enabled.");
+    if (form.useAuthentication && !form.preserveSensitivePassword && !form.password) {
+      errors.push("SMTP Password is required when authentication is enabled.");
+    }
+    if (form.password && form.password !== form.passwordConfirmation) errors.push("SMTP passwords do not match.");
+  }
+
+  if (form.type === "SNMP" || form.type === "Custom SNMP") {
+    if (list(form.snmpHosts).length === 0) errors.push("At least one SNMP host is required.");
+    if (!form.snmpCommunity?.trim()) errors.push("SNMP Community is required.");
+    if (!/^\d+$/.test(form.snmpPort?.trim() || "") || Number(form.snmpPort) < 1 || Number(form.snmpPort) > 65535) {
+      errors.push("SNMP Port must be between 1 and 65535.");
+    }
+    if (form.type === "Custom SNMP" && !form.snmpOid?.trim()) errors.push("Custom SNMP OID is required.");
+  }
+
+  if (form.type === "Alert Script" && !form.scriptFilename?.trim()) {
+    errors.push("Script Filename is required.");
+  }
+
+  const builtIns = new Set(notificationBuiltInKeys(form.type));
+  const names = new Set<string>();
+  for (const property of form.customProperties || []) {
+    const name = property.name.trim();
+    if (!/^[A-Za-z0-9._*-]+$/.test(name)) errors.push(`Invalid custom property name: ${name || "(empty)"}.`);
+    if (builtIns.has(name)) errors.push(`Custom property conflicts with a built-in property: ${name}.`);
+    if (names.has(name)) errors.push(`Duplicate custom property: ${name}.`);
+    names.add(name);
+  }
+  return errors;
+}
+
+export function buildAlertNotificationPayload(form: AlertNotificationForm): AlertNotificationPayload {
+  const properties: Record<string, unknown> = {};
+
+  if (form.type === "EMAIL") {
+    properties["ambari.dispatch.recipients"] = list(form.recipients);
+    properties["mail.smtp.host"] = form.smtpHost?.trim() || "";
+    properties["mail.smtp.port"] = form.smtpPort?.trim() || "";
+    properties["mail.smtp.from"] = form.emailFrom?.trim() || "";
+    properties["mail.smtp.auth"] = Boolean(form.useAuthentication);
+    if (form.useAuthentication) {
+      properties["ambari.dispatch.credential.username"] = form.username?.trim() || "";
+      const password = form.password || "";
+      if (password) {
+        properties[PASSWORD_KEY] = password;
+      } else if (form.preserveSensitivePassword && form.existingProperties && PASSWORD_KEY in form.existingProperties) {
+        properties[PASSWORD_KEY] = form.existingProperties[PASSWORD_KEY];
+      }
+      properties["mail.smtp.starttls.enable"] = Boolean(form.startTls);
+    }
+  } else if (form.type === "SNMP" || form.type === "Custom SNMP") {
+    properties["ambari.dispatch.snmp.version"] = form.snmpVersion || "SNMPv1";
+    properties["ambari.dispatch.snmp.community"] = form.snmpCommunity?.trim() || "";
+    properties["ambari.dispatch.recipients"] = list(form.snmpHosts);
+    properties["ambari.dispatch.snmp.port"] = form.snmpPort?.trim() || "";
+    if (form.type === "Custom SNMP") {
+      const oid = form.snmpOid?.trim() || "";
+      properties["ambari.dispatch.snmp.oids.trap"] = oid;
+      properties["ambari.dispatch.snmp.oids.subject"] = oid;
+      properties["ambari.dispatch.snmp.oids.body"] = oid;
+    }
+  } else {
+    const dispatchProperty = form.scriptDispatchProperty?.trim();
+    const filename = form.scriptFilename?.trim();
+    if (dispatchProperty) properties["ambari.dispatch-property.script"] = dispatchProperty;
+    if (filename) properties["ambari.dispatch-property.script.filename"] = filename;
+  }
+
+  for (const property of form.customProperties || []) {
+    properties[property.name.trim()] = property.value;
+  }
+
+  const target: AlertNotificationPayload["AlertTarget"] = {
+    name: form.name.trim(),
+    description: form.description,
+    global: form.global,
+    notification_type: notificationTypeToApi(form.type),
+    alert_states: [...form.alertStates],
+    properties,
+  };
+  if (!form.global) target.groups = [...form.groups];
+  return { AlertTarget: target };
+}
diff --git a/ambari-web/latest/src/screens/Alerts/constants.ts b/ambari-web/latest/src/Utils/alertPolicy.test.ts
similarity index 57%
copy from ambari-web/latest/src/screens/Alerts/constants.ts
copy to ambari-web/latest/src/Utils/alertPolicy.test.ts
index f8326c6ae6..45ebe14d3d 100644
--- a/ambari-web/latest/src/screens/Alerts/constants.ts
+++ b/ambari-web/latest/src/Utils/alertPolicy.test.ts
@@ -16,31 +16,22 @@
  * limitations under the License.
  */
 
-import {AlertStatus} from "./alertStatus";
+import { describe, expect, it } from "vitest";
+import { getAlertActionPolicy } from "./alertPolicy";
 
-export const STATUS_PRIORITY_ORDER = ['unknown', 'ok', 'warning', 'critical'];
-
-export const ALERT_SEARCH_CATEGORIES = [
-    'Status',
-    'Alert Definition Name',
-    'Service',
-    'Last Status Changed',
-    'State',
-    'Group',
-] as const;
-
-export const TIME_RANGES = {
-    'Past 1 hour': 1,
-    'Past 1 day': 24,
-    'Past 2 days': 48,
-    'Past 7 days': 168,
-    'Past 14 days': 336,
-    'Past 30 days': 720
-} as const;
-
-export const STATUS_OPTIONS = [
-    AlertStatus.OK,
-    AlertStatus.WARNING,
-    AlertStatus.CRITICAL,
-    AlertStatus.UNKNOWN
-] as const;
\ No newline at end of file
+describe("alert action policy", () => {
+  it("always exposes Groups and Settings while keeping independent gates", () => {
+    expect(getAlertActionPolicy(false, false)).toEqual({
+      create: false,
+      groups: true,
+      notifications: false,
+      settings: true,
+    });
+    expect(getAlertActionPolicy(true, true)).toEqual({
+      create: true,
+      groups: true,
+      notifications: true,
+      settings: true,
+    });
+  });
+});
diff --git a/ambari-web/latest/src/screens/Alerts/constants.ts b/ambari-web/latest/src/Utils/alertPolicy.ts
similarity index 57%
copy from ambari-web/latest/src/screens/Alerts/constants.ts
copy to ambari-web/latest/src/Utils/alertPolicy.ts
index f8326c6ae6..3eac6644f5 100644
--- a/ambari-web/latest/src/screens/Alerts/constants.ts
+++ b/ambari-web/latest/src/Utils/alertPolicy.ts
@@ -16,31 +16,21 @@
  * limitations under the License.
  */
 
-import {AlertStatus} from "./alertStatus";
+export interface AlertActionPolicy {
+  create: boolean;
+  groups: boolean;
+  notifications: boolean;
+  settings: boolean;
+}
 
-export const STATUS_PRIORITY_ORDER = ['unknown', 'ok', 'warning', 'critical'];
-
-export const ALERT_SEARCH_CATEGORIES = [
-    'Status',
-    'Alert Definition Name',
-    'Service',
-    'Last Status Changed',
-    'State',
-    'Group',
-] as const;
-
-export const TIME_RANGES = {
-    'Past 1 hour': 1,
-    'Past 1 day': 24,
-    'Past 2 days': 48,
-    'Past 7 days': 168,
-    'Past 14 days': 336,
-    'Past 30 days': 720
-} as const;
-
-export const STATUS_OPTIONS = [
-    AlertStatus.OK,
-    AlertStatus.WARNING,
-    AlertStatus.CRITICAL,
-    AlertStatus.UNKNOWN
-] as const;
\ No newline at end of file
+export function getAlertActionPolicy(
+  supportsCreateAlerts: boolean,
+  canManageNotifications: boolean,
+): AlertActionPolicy {
+  return {
+    create: supportsCreateAlerts,
+    groups: true,
+    notifications: canManageNotifications,
+    settings: true,
+  };
+}
diff --git a/ambari-web/latest/src/api/alertsApi.test.ts b/ambari-web/latest/src/api/alertsApi.test.ts
index 979e685ce2..ebe74f7dc4 100644
--- a/ambari-web/latest/src/api/alertsApi.test.ts
+++ b/ambari-web/latest/src/api/alertsApi.test.ts
@@ -42,4 +42,65 @@ describe("alerts API", () => {
       },
     });
   });
+
+  it("loads one definition directly by ID", async () => {
+    await AlertsApi.getAlertDefinitionById("cluster/name", "definition id", 1234);
+
+    expect(mocks.request).toHaveBeenCalledWith({
+      url: "/clusters/cluster%2Fname/alert_definitions/definition%20id",
+      method: "GET",
+      params: { fields: "*", _: 1234 },
+    });
+  });
+
+  it("loads instances for one definition", async () => {
+    await AlertsApi.getAlertInstancesByDefinition("cluster/name", 42, 1234);
+
+    expect(mocks.request).toHaveBeenCalledWith({
+      url: "/clusters/cluster%2Fname/alerts",
+      method: "GET",
+      params: {
+        fields: "*",
+        "Alert/definition_id": "42",
+        _: 1234,
+      },
+    });
+  });
+
+  it("loads the exact definition history window", async () => {
+    await AlertsApi.getAlertHistory("cluster/name", "definition name", 1000);
+
+    expect(mocks.request).toHaveBeenCalledWith({
+      url: "/clusters/cluster%2Fname/alert_history?(AlertHistory/definition_name=definition%20name)&(AlertHistory/timestamp>=1000)",
+      method: "GET",
+    });
+  });
+
+  it("creates a definition on the cluster collection", async () => {
+    const definition = { "AlertDefinition/name": "custom" };
+    await AlertsApi.createAlertDefinition("cluster/name", definition);
+
+    expect(mocks.request).toHaveBeenCalledWith({
+      url: "/clusters/cluster%2Fname/alert_definitions/",
+      method: "POST",
+      data: definition,
+    });
+  });
+
+  it("preserves complete group replacement payloads", async () => {
+    const payload = { AlertGroup: { name: "group", definitions: [1], targets: [2] } };
+    await AlertsApi.createAlertGroup("cluster", payload);
+    await AlertsApi.updateAlertGroup("cluster", 3, payload);
+
+    expect(mocks.request).toHaveBeenNthCalledWith(1, {
+      url: "clusters/cluster/alert_groups",
+      method: "POST",
+      data: payload,
+    });
+    expect(mocks.request).toHaveBeenNthCalledWith(2, {
+      url: "clusters/cluster/alert_groups/3",
+      method: "PUT",
+      data: payload,
+    });
+  });
 });
diff --git a/ambari-web/latest/src/api/alertsApi.ts b/ambari-web/latest/src/api/alertsApi.ts
index ae0589850d..7bfa12009a 100644
--- a/ambari-web/latest/src/api/alertsApi.ts
+++ b/ambari-web/latest/src/api/alertsApi.ts
@@ -113,6 +113,60 @@ export const AlertsApi = {
     });
     return response.data
   },
+
+  getAlertDefinitionById: async function (
+    clusterName: string,
+    definitionId: number | string,
+    time: number = Date.now(),
+  ) {
+    const response = await ambariApi.request({
+      url: `/clusters/${encodeURIComponent(clusterName)}/alert_definitions/${encodeURIComponent(String(definitionId))}`,
+      method: "GET",
+      params: { fields: "*", _: time },
+    });
+    return response.data;
+  },
+
+  getAlertInstancesByDefinition: async function (
+    clusterName: string,
+    definitionId: number | string,
+    time: number = Date.now(),
+  ) {
+    const response = await ambariApi.request({
+      url: `/clusters/${encodeURIComponent(clusterName)}/alerts`,
+      method: "GET",
+      params: {
+        fields: "*",
+        "Alert/definition_id": String(definitionId),
+        _: time,
+      },
+    });
+    return response.data;
+  },
+
+  getAlertHistory: async function (
+    clusterName: string,
+    definitionName: string,
+    since: number,
+  ) {
+    const url = `/clusters/${encodeURIComponent(clusterName)}/alert_history?` +
+      `(AlertHistory/definition_name=${encodeURIComponent(definitionName)})&` +
+      `(AlertHistory/timestamp>=${since})`;
+    const response = await ambariApi.request({ url, method: "GET" });
+    return response.data;
+  },
+
+  createAlertDefinition: async function (
+    clusterName: string,
+    definition: Record<string, unknown>,
+  ) {
+    const response = await ambariApi.request({
+      url: `/clusters/${encodeURIComponent(clusterName)}/alert_definitions/`,
+      method: "POST",
+      data: definition,
+    });
+    return response.data;
+  },
   
     getAlertsNotifications: async function (
         clusterName: string,
@@ -138,12 +192,10 @@ export const AlertsApi = {
     return response.data
 },
 
-  // ADDED: Get alerts with maintenance mode filtering (following Ember pattern)
   getGroupFormattedAlertsNotificationsWithMaintenanceFilter: async function (
     clusterName: string,
     time: number=Date.now())
 {
-    // Following Ember pattern: &Alert/maintenance_state.in(OFF) - only get alerts NOT in maintenance mode
     const url = `clusters/${clusterName}/alerts?format=groupedSummary&Alert/maintenance_state.in(OFF)&_=${time}`;
     const response = await ambariApi.request({
         url: url,
@@ -165,186 +217,22 @@ export const AlertsApi = {
 },
 
 createAlertGroup: async function (
-    clusterName: string,
-    groupData: {
-      AlertGroup: {
-        name: string;
-        definitions?: (number | { id: number | string })[];
-      }
-    }
-) {
-  try {
-    const url = `clusters/${clusterName}/alert_groups`;
-    
-    // Handle the definitions array properly
-    const definitionIds: number[] = [];
-    
-    if (groupData.AlertGroup.definitions && groupData.AlertGroup.definitions.length > 0) {
-      // Map to a completely new array with just numeric IDs
-      groupData.AlertGroup.definitions.forEach((def: number | { id: number | string }) => {
-        if (typeof def === 'number') {
-          definitionIds.push(def);
-        } else if (def && typeof def === 'object' && 'id' in def) {
-          // Push just the ID, not the entire object
-          definitionIds.push(Number(def.id));
-        }
-      });
-    }
-    
-    // Create the minimal request payload with exactly the format required by the API
-    const minimalPayload = JSON.stringify({
-      AlertGroup: {
-        name: groupData.AlertGroup.name,
-        ...(definitionIds.length > 0 && { definitions: definitionIds })
-      }
-    });
-    
-    console.log('Creating alert group with payload:', minimalPayload);
-    
-    const response = await ambariApi.request({
-      url: url,
-      method: "POST",
-      data: JSON.parse(minimalPayload)
-    });
-    return response.data;
-  } catch (error) {
-    console.error('Error in createAlertGroup:', error);
-    throw error;
-  }
-},
-
-deleteAlertGroup: async function (
-    clusterName: string,
-    groupId: number
+  clusterName: string,
+  groupData: { AlertGroup: { name: string; definitions: number[]; targets: number[] } },
 ) {
-  const url = `clusters/${clusterName}/alert_groups/${groupId}`;
   const response = await ambariApi.request({
-    url: url,
-    method: "DELETE"
-  });
-  return response.data;
-},
-
-renameAlertGroup: async function (
-    clusterName: string,
-    groupId: number,
-    newName: string
-) {
-  try {
-    // First get the current group data to get the definition IDs
-    const currentGroupUrl = `clusters/${clusterName}/alert_groups/${groupId}?fields=*`;
-    const currentGroup = await ambariApi.request({
-      url: currentGroupUrl,
-      method: "GET"
-    });
-
-    // Create a completely new array with just the IDs
-    const definitionIds: number[] = [];
-    if (currentGroup.data.AlertGroup.definitions) {
-      // Map to a completely new array with just numeric IDs
-      currentGroup.data.AlertGroup.definitions.forEach((def: number | { id: number | string }) => {
-        if (typeof def === 'number') {
-          definitionIds.push(def);
-        } else if (def && typeof def.id === 'number') {
-          // Push just the ID, not the entire object
-          definitionIds.push(Number(def.id));
-        }
-      });
-    }
-
-    // Create the minimal request payload with exactly the format required by the API
-    // Using a hardcoded structure to ensure no prototype inheritance or extra properties
-    const minimalPayload = JSON.stringify({
-      AlertGroup: {
-        name: newName,
-        definitions: definitionIds,
-        targets: []
-      }
-    });
-
-    console.log('Sending minimal request body:', minimalPayload);
-
-    const url = `clusters/${clusterName}/alert_groups/${groupId}`;
-    const response = await ambariApi.request({
-      url: url,
-      method: "PUT",
-      data: JSON.parse(minimalPayload)
-    });
-    return response.data;
-  } catch (error) {
-    console.error('Error in renameAlertGroup:', error);
-    throw error;
-  }
-},
-
-duplicateAlertGroup: async function (
-    clusterName: string,
-    sourceGroupId: number,
-    newGroupName: string
-) {
-  // First get the source group details
-  const sourceGroupUrl = `clusters/${clusterName}/alert_groups/${sourceGroupId}?fields=*`;
-  const sourceGroupResponse = await ambariApi.request({
-    url: sourceGroupUrl,
-    method: "GET"
-  });
-
-  const sourceGroup = sourceGroupResponse.data.AlertGroup;
-
-  // Create a completely new array with just the IDs
-  const definitionIds: number[] = [];
-  if (sourceGroup.definitions) {
-    // Map to a completely new array with just numeric IDs
-    sourceGroup.definitions.forEach((def: number | { id: number | string }) => {
-      if (typeof def === 'number') {
-        definitionIds.push(def);
-      } else if (def && typeof def === 'object' && 'id' in def) {
-        // Push just the ID, not the entire object
-        definitionIds.push(Number(def.id));
-      }
-    });
-  }
-
-  // Create new group with same definitions (but only IDs)
-  const createUrl = `clusters/${clusterName}/alert_groups`;
-  const data = {
-    AlertGroup: {
-      name: newGroupName,
-      definitions: definitionIds
-    }
-  };
-
-  console.log('Sending duplicate group request body:', JSON.stringify(data));
-
-  const response = await ambariApi.request({
-    url: createUrl,
+    url: `clusters/${clusterName}/alert_groups`,
     method: "POST",
-    data: data
+    data: groupData,
   });
-
   return response.data;
 },
 
-// Alert Definition Management within Groups
-addAlertDefinitionToGroup: async function (
-    clusterName: string,
-    groupId: number,
-    definitionId: number
-) {
-  const url = `clusters/${clusterName}/alert_groups/${groupId}/alert_definitions/${definitionId}`;
-  const response = await ambariApi.request({
-    url: url,
-    method: "POST"
-  });
-  return response.data;
-},
-
-removeAlertDefinitionFromGroup: async function (
+deleteAlertGroup: async function (
     clusterName: string,
-    groupId: number,
-    definitionId: number
+    groupId: number
 ) {
-  const url = `clusters/${clusterName}/alert_groups/${groupId}/alert_definitions/${definitionId}`;
+  const url = `clusters/${clusterName}/alert_groups/${groupId}`;
   const response = await ambariApi.request({
     url: url,
     method: "DELETE"
@@ -440,87 +328,18 @@ updateAlertDefinitionState: async function (
     return response.data;
   },
 
-  // Alert Group Notification Management
-addNotificationToGroup: async function (
-    clusterName: string,
-    groupId: number,
-    targetId: number
-) {
-  const url = `clusters/${clusterName}/alert_groups/${groupId}/alert_targets/${targetId}`;
-  const response = await ambariApi.request({
-    url: url,
-    method: "POST"
-  });
-  return response.data;
-},
-
-removeNotificationFromGroup: async function (
-    clusterName: string,
-    groupId: number,
-    targetId: number
+// Update entire alert group
+updateAlertGroup: async function (
+  clusterName: string,
+  groupId: number,
+  groupData: { AlertGroup: { name: string; definitions: number[]; targets: number[] } },
 ) {
-  const url = `clusters/${clusterName}/alert_groups/${groupId}/alert_targets/${targetId}`;
   const response = await ambariApi.request({
-    url: url,
-    method: "DELETE"
+    url: `clusters/${clusterName}/alert_groups/${groupId}`,
+    method: "PUT",
+    data: groupData,
   });
   return response.data;
-},
-
-// Update entire alert group
-updateAlertGroup: async function (
-    clusterName: string,
-    groupId: number,
-    groupData: {
-      AlertGroup: {
-        name: string;
-        definitions?: (number | { id: number })[];
-      }
-    }
-) {
-  try {
-    // Check if this is a temporary ID (from Date.now())
-    // If it's a large number (> 1000000), it's likely a temporary ID and we should skip the update
-    if (groupId > 1000000) {
-      console.warn('Skipping update for temporary group ID:', groupId);
-      return { success: false, message: 'Cannot update a temporary group. Save the group first.' };
-    }
-
-    // Create a completely new array with just the IDs
-    const definitionIds: number[] = [];
-    if (groupData.AlertGroup.definitions) {
-      // Map to a completely new array with just numeric IDs
-      groupData.AlertGroup.definitions.forEach((def: number | { id: number | string }) => {
-        if (typeof def === 'number') {
-          definitionIds.push(def);
-        } else if (def && typeof def === 'object' && 'id' in def) {
-          // Push just the ID, not the entire object
-          definitionIds.push(Number(def.id));
-        }
-      });
-    }
-
-    // Create the minimal request payload with exactly the format required by the API
-    const minimalPayload = JSON.stringify({
-      AlertGroup: {
-        name: groupData.AlertGroup.name,
-        definitions: definitionIds
-      }
-    });
-
-    console.log('Updating alert group with payload:', minimalPayload);
-
-    const url = `clusters/${clusterName}/alert_groups/${groupId}`;
-    const response = await ambariApi.request({
-      url: url,
-      method: "PUT",
-      data: JSON.parse(minimalPayload)
-    });
-    return response.data;
-  } catch (error) {
-    console.error('Error in updateAlertGroup:', error);
-    throw error;
-  }
   },
 
   // Get cluster configuration (including alert settings)
diff --git a/ambari-web/latest/src/components/EditableList.tsx b/ambari-web/latest/src/components/EditableList.tsx
index ba63249c47..55f510f847 100644
--- a/ambari-web/latest/src/components/EditableList.tsx
+++ b/ambari-web/latest/src/components/EditableList.tsx
@@ -49,28 +49,14 @@ const EditableList: React.FC<EditableListProps> = ({
 
   // Update typeahead when input changes
   useEffect(() => {
-    console.log('Typeahead update - Input:', input, 'Resources:', resources.length, 'Current items:', items.length);
-    
     if (input) {
       const inputValue = isCaseSensitive ? input : input.toLowerCase();
-      console.log('Filtering with input value:', inputValue);
-      
       const availableItems = resources.filter(resource => {
         const nameToCompare = isCaseSensitive ? resource.AlertTarget.name : resource.AlertTarget.name.toLowerCase();
         const alreadyExists = items.some(item => item.AlertTarget.name === resource.AlertTarget.name);
         const matchesInput = nameToCompare.indexOf(inputValue) >= 0;
-        
-        console.log('Checking resource:', resource.AlertTarget.name, {
-          nameToCompare,
-          matchesInput,
-          alreadyExists,
-          shouldInclude: matchesInput && !alreadyExists
-        });
-        
         return matchesInput && !alreadyExists;
       });
-      
-      console.log('Available items for typeahead:', availableItems.map(item => item.AlertTarget.name));
       setTypeahead(availableItems);
       setSelectedTypeahead(0);
     } else {
@@ -106,27 +92,11 @@ const EditableList: React.FC<EditableListProps> = ({
   };
 
   const addItem = (itemToAdd: AlertNotification) => {
-    console.log('Adding item to list:', itemToAdd.AlertTarget.name, itemToAdd);
-    console.log('Current items before adding:', items.map(item => item.AlertTarget.name));
-    console.log('Current input value before clearing:', input);
-    
     const updatedItems = [...items, itemToAdd];
-    console.log('Updated items after adding:', updatedItems.map(item => item.AlertTarget.name));
-    console.log('Calling onItemsChange with:', updatedItems);
-    
-    // Clear input and typeahead immediately
     setInput('');
     setTypeahead([]);
     setSelectedTypeahead(0);
-    console.log('Input, typeahead, and selection cleared');
-    
     onItemsChange(updatedItems);
-    
-    // Additional debug - check if items prop updates
-    setTimeout(() => {
-      console.log('Items prop after onItemsChange call:', items.map(item => item.AlertTarget.name));
-      console.log('Input value after timeout:', input);
-    }, 100);
   };
 
   const handleInputKeyDown = (event: React.KeyboardEvent) => {
@@ -146,14 +116,6 @@ const EditableList: React.FC<EditableListProps> = ({
     }
   };
 
-  // Debug logging
-  console.log('EditableList items:', items.map(item => ({
-    name: item.AlertTarget.name,
-    global: item.AlertTarget.global,
-    id: item.AlertTarget.id,
-    groups: item.AlertTarget.groups
-  })));
-
   return (
     <div className={`editable-list-container well ${editMode ? 'edit-mode' : ''}`}>
       <div className="items-box" onClick={!editMode ? enableEditMode : undefined}>
@@ -163,7 +125,6 @@ const EditableList: React.FC<EditableListProps> = ({
               <span>
                 <a href="#" onClick={(e) => e.preventDefault()}>
                   {item.AlertTarget.name}
-                  {/* Debug info */}
                   <small style={{ color: '#999', marginLeft: '5px' }}>
                     ({item.AlertTarget.global ? 'Global' : 'Custom'})
                   </small>
@@ -174,7 +135,6 @@ const EditableList: React.FC<EditableListProps> = ({
                     className="close"
                     onClick={(e) => {
                       e.stopPropagation();
-                      console.log('Removing notification:', item.AlertTarget.name, 'global:', item.AlertTarget.global);
                       removeFromItems(item);
                     }}
                   >
diff --git a/ambari-web/latest/src/router/RoutesList.tsx b/ambari-web/latest/src/router/RoutesList.tsx
index 865fcfb6d0..a5a5863f65 100644
--- a/ambari-web/latest/src/router/RoutesList.tsx
+++ b/ambari-web/latest/src/router/RoutesList.tsx
@@ -34,6 +34,7 @@ import HostsList from "../screens/Hosts/HostsList";
 import { Hosts } from "../screens/Hosts";
 import Alerts from "../screens/Alerts/Alerts";
 import AlertDefinitionDetails from "../screens/Alerts/AlertDefinitionDetails";
+import AlertDefinitionWizard from "../screens/Alerts/AlertDefinitionWizard";
 import ServiceAutoStart from "../screens/ServiceAutoStart";
 import ServiceAccounts from "../screens/ServiceAccounts";
 import EnableKerberos from "../screens/KerberosWizard/EnableKerberos";
@@ -215,6 +216,17 @@ const RoutesList: RouteObject[] = [
                 ),
               },
               { path: "alerts", element: <Alerts /> },
+              {
+                path: "alerts/add/:stepNumber",
+                element: (
+                  <ProtectedRoute
+                    requireAuthorization="SERVICE.TOGGLE_ALERTS"
+                    redirectTo="/main/alerts"
+                  >
+                    <AlertDefinitionWizard />
+                  </ProtectedRoute>
+                ),
+              },
               { path: "alerts/:alertId", element: <AlertDefinitionDetails /> },
               {
                 path: "admin",
diff --git a/ambari-web/latest/src/screens/Alerts/ActionsButton.tsx b/ambari-web/latest/src/screens/Alerts/ActionsButton.tsx
index 7d082dc0b5..c37c49ac50 100644
--- a/ambari-web/latest/src/screens/Alerts/ActionsButton.tsx
+++ b/ambari-web/latest/src/screens/Alerts/ActionsButton.tsx
@@ -19,9 +19,9 @@
 import React, { useState, useContext, useEffect } from 'react';
 import Modal from '../../components/Modal';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faEnvelope, faGear, faTh } from '@fortawesome/free-solid-svg-icons';
+import { faEnvelope, faGear, faPlus, faTh } from '@fortawesome/free-solid-svg-icons';
 import { AlertGroupItem, AlertDefinition, AlertNotification, AlertGroupState } from './types';
-import { useParams } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
 import { AppContext } from '../../store/context';
 import { 
   ManageAlertGroupsModal, 
@@ -43,6 +43,22 @@ import { useAuth } from '../../hooks/useAuth';
 import ClusterApi from '../../api/clusterApi';
 import ConfigsApi from '../../api/configsApi';
 import { ambariApi } from '../../api/config/axiosConfig';
+import { getAlertActionPolicy } from '../../Utils/alertPolicy';
+import {
+  executeAlertGroupSave,
+  planAlertGroupSave,
+  reconcileAlertGroupSave,
+  reconcileAlertGroupSaveWithServer,
+  validateAlertGroupName,
+} from '../../Utils/alertGroups';
+import {
+  buildAlertNotificationPayload,
+  notificationBuiltInKeys,
+  notificationTypeToUi,
+  validateAlertNotificationForm,
+  type AlertNotificationForm,
+} from '../../Utils/alertNotifications';
+import { validateRepeatTolerance } from '../../Utils/alertDefinitions';
 
 interface ActionsButtonProps {
   alertGroups: AlertGroupItem[];
@@ -52,20 +68,19 @@ interface ActionsButtonProps {
 
 export const ActionsButton: React.FC<ActionsButtonProps> = ({ 
   alertGroups: initialAlertGroups, 
-  allAlertDefinitions}) => {
+  allAlertDefinitions,
+  onModalStateChange}) => {
   // Context and URL params
   const { clusterName: urlClusterName } = useParams<{ clusterName: string }>();
-  const { clusterName: contextClusterName, upgradeIsRunning, upgradeSuspended } = useContext(AppContext);
+  const { clusterName: contextClusterName, supports } = useContext(AppContext);
+  const navigate = useNavigate();
   
   // Authorization hooks - implementing Ember.js alert authorization patterns
   const { hasAuthorization } = useAuth();
   
   // Check specific authorizations for alert operations
   const canManageNotifications = hasAuthorization('CLUSTER.MANAGE_ALERT_NOTIFICATIONS');
-  
-  // Check if upgrade is blocking operations (running but not suspended)
-  // During upgrades, alert actions should be disabled to match legacy UI behavior
-  const isUpgradeBlocking = upgradeIsRunning && !upgradeSuspended;
+  const actionPolicy = getAlertActionPolicy(Boolean(supports.createAlerts), canManageNotifications);
   
   const [clusterName] = useState<string>(urlClusterName || contextClusterName || '');
   
@@ -83,6 +98,8 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   const [isSaveDisabled, setIsSaveDisabled] = useState<boolean>(true);
   const [modalError, setModalError] = useState<string>('');
   const [modalSuccess, setModalSuccess] = useState<string>('');
+  const [groupLoadError, setGroupLoadError] = useState<string>('');
+  const [notificationLoadError, setNotificationLoadError] = useState<string>('');
 
   // State for notifications
   const [notifications, setNotifications] = useState<AlertNotification[]>([]);
@@ -125,7 +142,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   const [scriptDispatchProperty, setScriptDispatchProperty] = useState<string>('');
   
   // State for alert settings
-  const [alertCheckCount, setAlertCheckCount] = useState(1);
+  const [alertCheckCount, setAlertCheckCount] = useState('1');
   
   // State for modals
   const [modalContent, setModalContent] = useState<string>('');
@@ -162,6 +179,25 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   // State for showing the main modal
   const [showMainModal, setShowMainModal] = useState<boolean>(false);
 
+  useEffect(() => {
+    onModalStateChange?.(
+      showMainModal || showNewModal || showAddDefinitionModal || showRenameModal ||
+      showDuplicateModal || showDeleteConfirmation || showDeleteNotificationModal ||
+      showNotificationFormModal || showResultsModal,
+    );
+  }, [
+    onModalStateChange,
+    showAddDefinitionModal,
+    showDeleteConfirmation,
+    showDeleteNotificationModal,
+    showDuplicateModal,
+    showMainModal,
+    showNewModal,
+    showNotificationFormModal,
+    showRenameModal,
+    showResultsModal,
+  ]);
+
   // Helper function to get group name
   const getGroupName = (group: AlertGroupItem | AlertGroupState | null): string => {
     if (!group) return '';
@@ -181,8 +217,9 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
 
   // Check if a group name already exists
   const groupNameExists = (name: string): boolean => {
+    const normalized = name.trim().toLowerCase();
     return alertGroups.some(group => 
-      group.AlertGroup.name.toLowerCase() === name.toLowerCase() && 
+      group.AlertGroup.name.trim().toLowerCase() === normalized &&
       !group.AlertGroup._deleted
     );
   };
@@ -533,9 +570,8 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       setErrorMessage('');
       setModalError('');
       
-      if (!newGroupName.trim()) {
-        throw new Error('Group name cannot be empty');
-      }
+      const nameError = validateAlertGroupName(newGroupName);
+      if (nameError) throw new Error(nameError);
       
       // Check if a group with this name already exists
       if (groupNameExists(newGroupName)) {
@@ -551,9 +587,10 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       const newGroup: AlertGroupItem = {
         AlertGroup: {
           id: tempId,
-          name: newGroupName,
+          name: newGroupName.trim(),
           default: false,
           definitions: [],
+          targets: [],
           _isNew: true,
           _isModified: false,
           _deleted: false,
@@ -602,9 +639,8 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
     }
     
     try {
-      if (!duplicateGroupName.trim()) {
-        throw new Error('Group name cannot be empty');
-      }
+      const nameError = validateAlertGroupName(duplicateGroupName);
+      if (nameError) throw new Error(nameError);
       
       // Check if a group with this name already exists
       if (groupNameExists(duplicateGroupName)) {
@@ -619,15 +655,19 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       const definitionsToDuplicate = 'AlertGroup' in groupToDuplicate 
         ? [...(groupToDuplicate.AlertGroup.definitions || [])]
         : [...(groupToDuplicate.definitions || [])];
+      const targetsToDuplicate = 'AlertGroup' in groupToDuplicate
+        ? [...(groupToDuplicate.AlertGroup.targets || [])]
+        : [...(groupToDuplicate.targets || [])];
       
       
       // Create a new group with the duplicated data
       const newGroup: AlertGroupItem = {
         AlertGroup: {
           id: tempId,
-          name: duplicateGroupName,
+          name: duplicateGroupName.trim(),
           default: false,
           definitions: definitionsToDuplicate,
+          targets: targetsToDuplicate,
           _isNew: true,
           _isModified: false,
           _deleted: false,
@@ -675,8 +715,11 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
     
     try {
       const currentName = getGroupName(groupToRename);
+      const normalizedName = renameGroupName.trim();
+      const nameError = validateAlertGroupName(renameGroupName);
+      if (nameError) throw new Error(nameError);
       
-      if (!renameGroupName || renameGroupName === currentName) {
+      if (normalizedName === currentName.trim()) {
         handleCloseModal();
         return; // User didn't change the name
       }
@@ -684,7 +727,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       // Check if a group with this name already exists (excluding the current group)
       const groupId = 'AlertGroup' in groupToRename ? groupToRename.AlertGroup.id : groupToRename.id;
       const nameExists = alertGroups.some(group => 
-        group.AlertGroup.name.toLowerCase() === renameGroupName.toLowerCase() && 
+        group.AlertGroup.name.trim().toLowerCase() === normalizedName.toLowerCase() &&
         group.AlertGroup.id !== groupId &&
         !group.AlertGroup._deleted
       );
@@ -700,7 +743,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
           ...groupToRename,
           AlertGroup: {
             ...groupToRename.AlertGroup,
-            name: renameGroupName,
+            name: normalizedName,
             _isModified: true
           }
         };
@@ -729,7 +772,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
         // Handle AlertGroupState structure
         const updatedGroup = {
           ...groupToRename,
-          name: renameGroupName,
+          name: normalizedName,
           _isModified: true
         };
         
@@ -743,7 +786,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
                 ...g,
                 AlertGroup: {
                   ...g.AlertGroup,
-                  name: renameGroupName,
+                  name: normalizedName,
                   _isModified: true
                 }
               };
@@ -809,13 +852,17 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
 
   // Handle save global alert settings
   const handleSaveGlobalAlertSettings = async (): Promise<void> => {
+    const validationError = validateRepeatTolerance(alertCheckCount);
+    if (validationError) {
+      setModalError(validationError);
+      return;
+    }
+
     try {
       setLoading(true);
       setModalError('');
       setModalSuccess('');
       
-      console.log('Saving global alert check count:', alertCheckCount);
-      
       // Update the cluster-env configuration with the new alerts_repeat_tolerance value
       const response = await ClusterApi.getCluster(clusterName);
       const desiredConfigs = response?.Clusters?.desired_configs;
@@ -840,7 +887,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
           // This prevents data loss by preserving all existing configuration
           const updatedProperties = {
             ...existingProperties,
-            alerts_repeat_tolerance: alertCheckCount.toString()
+            alerts_repeat_tolerance: alertCheckCount
           };
 
           // Save the updated configuration using the proper service config API structure
@@ -884,216 +931,76 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       setLoading(true);
       setErrorMessage('');
       setModalError('');
-      
-      
-      // Find groups that need to be created, updated, or deleted
-      const groupsToCreate: AlertGroupItem[] = [];
-      const groupsToUpdate: AlertGroupItem[] = [];
-      const groupsToDelete: AlertGroupItem[] = [];
-      
-      alertGroups.forEach(group => {
-        
-        // Determine which operation to perform
-        if (group.AlertGroup._deleted) {
-          // Add to delete list regardless of ID - we'll filter out negative IDs later
-          groupsToDelete.push(group);
-        } else if (group.AlertGroup._isNew) {
-          groupsToCreate.push(group);
-        } else if (group.AlertGroup._isModified) {
-          // Only add to update list if it's an existing group (has a positive ID)
-          if (group.AlertGroup.id > 0) {
-            groupsToUpdate.push(group);
-          }
-        }
-      });
-      
-      
-      // Store counts for the results modal - do this BEFORE making API calls
-      // to ensure we show the correct counts even if some API calls fail
-      const newCount = groupsToCreate.length;
-      const updatedCount = groupsToUpdate.length;
-      
-      // For deletes, only count groups with positive IDs that need to be deleted on the server
-      const groupsToDeleteOnServer = groupsToDelete.filter(group => group.AlertGroup.id > 0);
-      const removedCount = groupsToDeleteOnServer.length;
-      
-      
-      // Save the counts to state variables
-      setResultsNewCount(newCount);
-      setResultsUpdatedCount(updatedCount);
-      setResultsRemovedCount(removedCount);
-      
-      // Also save to a separate state variable to ensure we have the values after the modal closes
+      const plan = planAlertGroupSave(alertGroups);
+      const newCount = plan.create.length;
+      const updatedCount = plan.update.length;
+      const removedCount = plan.delete.length;
+
       setPendingChanges({
         create: newCount,
         update: updatedCount,
         delete: removedCount
       });
-      
-      // Track successful and failed operations
-      const successfulOperations = {
-        create: 0,
-        update: 0,
-        delete: 0
-      };
-      
-      const failedOperations = {
-        create: 0,
-        update: 0,
-        delete: 0
-      };
-      
-      // Process each type of operation separately
-      
-      // 0. Process notification changes for modified groups
-      for (const group of groupsToUpdate) {
-        try {
-          const groupId = group.AlertGroup.id;
-          
-          // Get current notifications for this group
-          //@ts-ignore
-          const currentGroupNotifications = notifications.filter(notification => {
-            const target = notification.AlertTarget;
-            if (target.global) return true;
-            if (target.groups && Array.isArray(target.groups)) {
-              return target.groups.includes(groupId);
-            }
-            return false;
-          });
-          
-          
-        } catch (error) {
-          console.error('Error processing notification changes for group:', group.AlertGroup.id, error);
-        }
-      }
-      
-      // 1. Create operations
-      for (const group of groupsToCreate) {
-        try {
-          const { name, definitions = [] } = group.AlertGroup;
-          const definitionIds = definitions.map(def => typeof def === 'number' ? def : def.id);
-          
-          // Create request payload - only include definitions if there are any
-          const payload: any = {
-            AlertGroup: {
-              name
-            }
-          };
-          
-          // Only add definitions if there are any
-          if (definitionIds.length > 0) {
-            payload.AlertGroup.definitions = definitionIds;
-          }
-          
-          
-          await AlertsApi.createAlertGroup(clusterName, payload);
-          successfulOperations.create++;
-        } catch (error) {
-          console.error('Error creating group:', error);
-          failedOperations.create++;
-        }
-      }
-      
-      // 2. Update operations
-      for (const group of groupsToUpdate) {
-        try {
-          const { id, name, definitions = [] } = group.AlertGroup;
-          
-          const definitionIds = definitions.map(def => typeof def === 'number' ? def : def.id);
-          
-          // Create request payload - only include definitions if there are any
-          const payload: any = {
-            AlertGroup: {
-              name
-            }
-          };
-          
-          // Only add definitions if there are any
-          if (definitionIds.length > 0) {
-            payload.AlertGroup.definitions = definitionIds;
-          }
-          
-          
-          await AlertsApi.updateAlertGroup(clusterName, id, payload);
-          successfulOperations.update++;
-        } catch (error) {
-          console.error('Error updating group:', error);
-          failedOperations.update++;
-        }
+      const result = await executeAlertGroupSave(plan, {
+        create: (_group, payload) => AlertsApi.createAlertGroup(clusterName, payload),
+        update: (group, payload) => AlertsApi.updateAlertGroup(clusterName, group.AlertGroup.id, payload),
+        delete: (group) => AlertsApi.deleteAlertGroup(clusterName, group.AlertGroup.id),
+      });
+
+      const locallyReconciledGroups = reconcileAlertGroupSave(alertGroups, result.failures);
+      let serverGroups: AlertGroupItem[] | null = null;
+      try {
+        const response = await AlertsApi.getAlertGroups(clusterName);
+        if (!Array.isArray(response?.items)) throw new Error('Invalid Alert Groups response');
+        serverGroups = response.items.map((item: any) => ({ AlertGroup: item.AlertGroup }));
+      } catch (error) {
+        console.error('Error refreshing alert groups after save:', error);
       }
-      
-      // 3. Delete operations
-      for (const group of groupsToDeleteOnServer) {
-        try {
-          const { id } = group.AlertGroup;
-          
-          
-          await AlertsApi.deleteAlertGroup(clusterName, id);
-          successfulOperations.delete++;
-        } catch (error) {
-          console.error('Error deleting group:', error);
-          failedOperations.delete++;
-        }
+
+      let reconciledGroups = locallyReconciledGroups;
+      let remainingFailures = result.failures;
+      if (serverGroups && result.failures.length > 0) {
+        const reconciliation = reconcileAlertGroupSaveWithServer(
+          serverGroups,
+          alertGroups,
+          result.failures,
+        );
+        reconciledGroups = reconciliation.groups;
+        remainingFailures = reconciliation.failures;
       }
-      
-      
-      // Check if there were any failures
-      const hasFailures = 
-        failedOperations.create > 0 || 
-        failedOperations.update > 0 || 
-        failedOperations.delete > 0;
-      
-      if (hasFailures) {
-        // Show error message about partial success
-        const errorMsg = `Some operations failed: ${failedOperations.create} creations, ${failedOperations.update} updates, ${failedOperations.delete} deletions failed.`;
+
+      if (remainingFailures.length > 0) {
+        const failedCounts = remainingFailures.reduce((counts, failure) => {
+          counts[failure.kind] += 1;
+          return counts;
+        }, { create: 0, update: 0, delete: 0 });
+        const errorMsg = `Some operations failed: ${failedCounts.create} creations, ${failedCounts.update} updates, ${failedCounts.delete} deletions failed. Review the pending changes and try again.${serverGroups ? '' : ' The server refresh also failed; only failed operations remain pending locally.'}`;
+        setAlertGroups(reconciledGroups);
+        setSelectedGroup(null);
         setErrorMessage(errorMsg);
         setModalError(errorMsg);
-        
-        // Clear error message after a delay
-        setTimeout(() => {
-          setErrorMessage('');
-          setModalError('');
-        }, 5000);
+        setIsSaveDisabled(false);
+        return;
       }
-      
-      // Reset the _isNew, _isModified, and _deleted flags and remove deleted groups
-      const updatedGroups = alertGroups
-        .filter(group => !group.AlertGroup._deleted) // Remove deleted groups
-        .map(group => ({
-          ...group,
-          AlertGroup: {
-            ...group.AlertGroup,
-            _isNew: false,
-            _isModified: false
-          }
-        }));
-      
-      
-      // Update the state with the new groups
-      setAlertGroups(updatedGroups);
-      
-      // Close the main modal
+
+      if (!serverGroups) {
+        setAlertGroups(locallyReconciledGroups);
+        setSelectedGroup(null);
+        setGroupLoadError('The changes were saved, but Alert Groups could not be reloaded. Retry the load before closing.');
+        setIsSaveDisabled(true);
+        return;
+      }
+
+      setAlertGroups(serverGroups);
+      setSelectedGroup(null);
       setShowMainModal(false);
-      
-      // Disable save button
       setIsSaveDisabled(true);
-      
-      // Only show the results modal if there were actual changes
-      const hasChanges = newCount > 0 || updatedCount > 0 || removedCount > 0;
-      
-      
-      if (hasChanges) {
-        
-        // Update the counts to show only successful operations
-        setResultsNewCount(successfulOperations.create);
-        setResultsUpdatedCount(successfulOperations.update);
-        setResultsRemovedCount(successfulOperations.delete);
-        
-        // Use a longer delay to ensure the first modal is fully closed
-        setTimeout(() => {
-          setShowResultsModal(true);
-        }, 500);
-      } else {
+
+      if (newCount > 0 || updatedCount > 0 || removedCount > 0) {
+        setResultsNewCount(newCount);
+        setResultsUpdatedCount(updatedCount);
+        setResultsRemovedCount(removedCount);
+        setTimeout(() => setShowResultsModal(true), 500);
       }
     } catch (error: any) {
       console.error('Error saving alert groups:', error);
@@ -1103,11 +1010,6 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       setErrorMessage(errorMsg);
       setModalError(errorMsg);
       
-      // Clear error message after a delay
-      setTimeout(() => {
-        setErrorMessage('');
-        setModalError('');
-      }, 5000);
     } finally {
       setLoading(false);
     }
@@ -1214,10 +1116,6 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       });
       
       
-      // Count how many groups are marked for deletion
-      //@ts-ignore
-      const deletedCount = updatedGroups.filter(g => g.AlertGroup._deleted).length;
-      
       setAlertGroups(updatedGroups);
 
       // Reset states
@@ -1259,11 +1157,10 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   // Handle setting new group name with validation
   const handleNewGroupNameChange = (name: string): void => {
     setNewGroupName(name);
-    
-    // Check if name is empty
-    if (!name.trim()) {
+    const nameError = validateAlertGroupName(name);
+    if (nameError) {
       setIsNewNameValid(false);
-      setModalError('Group name cannot be empty');
+      setModalError(nameError);
       return;
     }
     
@@ -1280,11 +1177,10 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   // Handle setting rename group name with validation
   const handleRenameGroupNameChange = (name: string): void => {
     setRenameGroupName(name);
-    
-    // Check if name is empty
-    if (!name.trim()) {
+    const nameError = validateAlertGroupName(name);
+    if (nameError) {
       setIsRenameValid(false);
-      setModalError('Group name cannot be empty');
+      setModalError(nameError);
       return;
     }
     
@@ -1295,7 +1191,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
     
     // Check if name already exists (excluding the current group)
     const nameExists = alertGroups.some(group => 
-      group.AlertGroup.name.toLowerCase() === name.toLowerCase() && 
+      group.AlertGroup.name.trim().toLowerCase() === name.trim().toLowerCase() &&
       group.AlertGroup.id !== currentGroupId &&
       !group.AlertGroup._deleted
     );
@@ -1312,11 +1208,10 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   // Handle setting duplicate group name with validation
   const handleDuplicateGroupNameChange = (name: string): void => {
     setDuplicateGroupName(name);
-    
-    // Check if name is empty
-    if (!name.trim()) {
+    const nameError = validateAlertGroupName(name);
+    if (nameError) {
       setIsDuplicateNameValid(false);
-      setModalError('Group name cannot be empty');
+      setModalError(nameError);
       return;
     }
     
@@ -1339,16 +1234,24 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
 
   // Handle manage alert groups
   const handleManageAlertGroups = (): void => {
+    setModalError('');
+    setGroupLoadError('');
+    setNotificationLoadError('');
     toggleModalContent('Manage Alert Groups');
+    void Promise.all([loadAlertGroups(), loadNotifications()]);
   };
 
   // Handle manage notifications
   const handleManageNotifications = (): void => {
+    setModalError('');
+    setNotificationLoadError('');
     toggleModalContent('Manage Notifications');
+    void loadNotifications();
   };
 
   // Handle manage alert settings
   const handleManageAlertSettings = (): void => {
+    setIsSaveDisabled(true);
     toggleModalContent('Manage Alert Settings');
   };
 
@@ -1374,11 +1277,17 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
             allAlertDefinitions={allAlertDefinitions}
             loadAvailableDefinitions={loadAvailableDefinitions}
             setShowAddDefinitionModal={setShowAddDefinitionModal}
-            errorMessage={modalError}
+            errorMessage={groupLoadError || notificationLoadError || modalError}
             successMessage={modalSuccess}
             notifications={notifications}
             onNotificationsChange={handleNotificationsChange}
             onManageNotifications={handleManageNotifications}
+            onRetry={groupLoadError || notificationLoadError ? () => {
+              setGroupLoadError('');
+              setNotificationLoadError('');
+              setModalError('');
+              void Promise.all([loadAlertGroups(), loadNotifications()]);
+            } : undefined}
           />
         );
       case 'Manage Notifications':
@@ -1394,10 +1303,15 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
             handleDuplicate={handleDuplicateNotification}
             handleToggleEnabled={handleToggleNotificationEnabled}
             handleCreateNotification={handleCreateNotification}
-            errorMessage={modalError}
+            errorMessage={notificationLoadError || modalError}
             successMessage={modalSuccess}
             setErrorMessage={setModalError}
             setSuccessMessage={setModalSuccess}
+            onRetry={notificationLoadError ? () => {
+              setNotificationLoadError('');
+              setModalError('');
+              void loadNotifications();
+            } : undefined}
             alertGroups={alertGroups
               .filter(group => !group.AlertGroup._deleted)
               .map(group => ({ 
@@ -1412,7 +1326,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
         return (
           <ManageAlertSettingsModal
             alertCheckCount={alertCheckCount}
-            setAlertCheckCount={setAlertCheckCount as any}
+            setAlertCheckCount={setAlertCheckCount}
             setIsSaveDisabled={setIsSaveDisabled}
             modalError={modalError}
             modalSuccess={modalSuccess}
@@ -1423,6 +1337,22 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
     }
   };
 
+  const loadAlertGroups = async (): Promise<void> => {
+    if (!clusterName) return;
+
+    try {
+      setLoading(true);
+      const response = await AlertsApi.getAlertGroups(clusterName);
+      setAlertGroups((response?.items || []).map((item: any) => ({ AlertGroup: item.AlertGroup })));
+      setGroupLoadError('');
+    } catch (error) {
+      console.error('Error loading alert groups:', error);
+      setGroupLoadError('Failed to load alert groups.');
+    } finally {
+      setLoading(false);
+    }
+  };
+
   // Load notifications
   const loadNotifications = async (): Promise<void> => {
     if (!clusterName) return;
@@ -1440,22 +1370,23 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
           return {
             AlertTarget: {
               ...alertTarget,
-              is_enabled: alertTarget.enabled,
+              is_enabled: alertTarget.is_enabled ?? alertTarget.enabled,
               // If groups is an array of objects, keep it as is
               // The API returns groups as an array of objects, but our code expects it as an array of numbers
               // when sending data back to the API
               groups: Array.isArray(alertTarget.groups) 
-                ? alertTarget.groups.map((group: any) => group.id)
+                ? alertTarget.groups.map((group: any) => typeof group === 'number' ? group : group.id)
                 : alertTarget.groups
             }
           };
         });
         
         setNotifications(fetchedNotifications);
+        setNotificationLoadError('');
       }
     } catch (error) {
       console.error('Error loading notifications:', error);
-      setModalError('Failed to load notifications');
+      setNotificationLoadError('Failed to load notifications.');
     } finally {
       setLoading(false);
     }
@@ -1491,12 +1422,12 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
         ) {
           const clusterEnvConfig = configResponse.data.items[0];
           const globalCheckCount = clusterEnvConfig.properties?.alerts_repeat_tolerance || "1";
-          setAlertCheckCount(parseInt(globalCheckCount) || 1);
+          setAlertCheckCount(String(globalCheckCount));
         }
       }
     } catch (error) {
       console.error('Error loading global alert settings:', error);
-      setAlertCheckCount(1);
+      setAlertCheckCount('1');
     } finally {
     }
   };
@@ -1508,6 +1439,8 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   
   // Handle notification deletion modal
   const handleDeleteNotificationModalShow = (notification: AlertNotification): void => {
+    setModalError('');
+    setModalSuccess('');
     setSelectedNotification(notification);
     setShowDeleteNotificationModal(true);
   };
@@ -1555,7 +1488,8 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       setSelectedNotification(notification);
       setNotificationName(notification.AlertTarget.name);
       setNotificationDescription(notification.AlertTarget.description || '');
-      setNotificationType(notification.AlertTarget.notification_type);
+      const uiType = notificationTypeToUi(notification.AlertTarget.notification_type);
+      setNotificationType(uiType);
       
       // Reset all type-specific fields first
       setNotificationRecipients('');
@@ -1576,7 +1510,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       setScriptDispatchProperty('');
       
       // Set type-specific fields based on notification type
-      if (notification.AlertTarget.notification_type === 'EMAIL') {
+      if (uiType === 'EMAIL') {
         // Handle recipients which may be an array or string
         const recipients = notification.AlertTarget.properties?.['ambari.dispatch.recipients'];
         if (recipients) {
@@ -1593,7 +1527,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
         setSmtpPort(notification.AlertTarget.properties?.['mail.smtp.port'] || '');
         setEmailFrom(notification.AlertTarget.properties?.['mail.smtp.from'] || '');
         
-        const hasAuth = notification.AlertTarget.properties?.['mail.smtp.auth'] === true;
+        const hasAuth = String(notification.AlertTarget.properties?.['mail.smtp.auth']) === 'true';
         setUseAuthentication(hasAuth);
         setUsername(notification.AlertTarget.properties?.['ambari.dispatch.credential.username'] || '');
         if (notification.AlertTarget.properties?.['ambari.dispatch.credential.password']) {
@@ -1602,19 +1536,20 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
           setPassword('');
         }
         setPasswordConfirmation('');
-        setStartTls(notification.AlertTarget.properties?.['mail.smtp.starttls.enable'] === true);
-      } else if (notification.AlertTarget.notification_type === 'SNMP' || notification.AlertTarget.notification_type === 'Custom SNMP') {
+        setStartTls(String(notification.AlertTarget.properties?.['mail.smtp.starttls.enable']) === 'true');
+      } else if (uiType === 'SNMP' || uiType === 'Custom SNMP') {
         setSnmpVersion(notification.AlertTarget.properties?.['ambari.dispatch.snmp.version'] || 'SNMPv1');
         setSnmpCommunity(notification.AlertTarget.properties?.['ambari.dispatch.snmp.community'] || '');
-        setSnmpHosts(notification.AlertTarget.properties?.['ambari.dispatch.snmp.host'] || '');
+        const recipients = notification.AlertTarget.properties?.['ambari.dispatch.recipients'];
+        setSnmpHosts(Array.isArray(recipients) ? recipients.join(',') : String(recipients || ''));
         setSnmpPort(notification.AlertTarget.properties?.['ambari.dispatch.snmp.port'] || '');
         
-        if (notification.AlertTarget.notification_type === 'Custom SNMP') {
-          setSnmpOid(notification.AlertTarget.properties?.['ambari.dispatch.snmp.oid'] || '');
+        if (uiType === 'Custom SNMP') {
+          setSnmpOid(notification.AlertTarget.properties?.['ambari.dispatch.snmp.oids.trap'] || '');
         }
-      } else if (notification.AlertTarget.notification_type === 'Alert Script') {
-        setScriptFilename(notification.AlertTarget.properties?.['ambari.dispatch-property.script_file_name'] || '');
-        setScriptDispatchProperty(notification.AlertTarget.properties?.['ambari.dispatch.script.parameter.1'] || '');
+      } else if (uiType === 'Alert Script') {
+        setScriptFilename(notification.AlertTarget.properties?.['ambari.dispatch-property.script.filename'] || '');
+        setScriptDispatchProperty(notification.AlertTarget.properties?.['ambari.dispatch-property.script'] || '');
       }
       
       // Set notification enabled state
@@ -1648,14 +1583,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       // Set custom properties
       const customProps: {name: string, value: string}[] = [];
       if (notification.AlertTarget.properties) {
-        const standardProps = [
-          'ambari.dispatch.recipients', 'mail.smtp.host', 'mail.smtp.port', 'mail.smtp.from',
-          'mail.smtp.auth', 'ambari.dispatch.credential.username', 'ambari.dispatch.credential.password',
-          'mail.smtp.starttls.enable', 'ambari.dispatch.snmp.version', 'ambari.dispatch.snmp.community',
-          'ambari.dispatch.snmp.host', 'ambari.dispatch.snmp.port', 'ambari.dispatch.snmp.oid',
-          'ambari.dispatch-property.script_file_name', 'ambari.dispatch.script.filename',
-          'ambari.dispatch.script.parameter.1'
-        ];
+        const standardProps = notificationBuiltInKeys(uiType);
         
         Object.entries(notification.AlertTarget.properties).forEach(([key, value]) => {
           if (!standardProps.includes(key)) {
@@ -1683,9 +1611,6 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
       setNotificationFormMode('duplicate');
       setNotificationName(`Copy of ${notification.AlertTarget.name}`);
       
-      // Clear the selected notification since we're creating a new one
-      setSelectedNotification(null);
-      
     } catch (error) {
       console.error('Error duplicating notification:', error);
       setModalError('Failed to duplicate notification');
@@ -1758,188 +1683,64 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
 
   // Handle notification form submission
   const handleNotificationFormSubmit = async (): Promise<void> => {
-    if (!notificationName.trim()) {
-      setModalError('Notification name is required');
-      return;
-    }
-
     try {
-      setLoading(true);
       setModalError('');
-
-      // Build properties object
-      const properties: Record<string, any> = {};
-      
-      // Add standard properties based on notification type
-      if (notificationType === 'EMAIL') {
-        // Format recipients as an array
-        properties['ambari.dispatch.recipients'] = notificationRecipients.split(',').map(r => r.trim());
-        properties['mail.smtp.host'] = smtpServer;
-        properties['mail.smtp.port'] = smtpPort;
-        properties['mail.smtp.from'] = emailFrom;
-        
-        if (useAuthentication) {
-          properties['mail.smtp.auth'] = true;
-          properties['ambari.dispatch.credential.username'] = username;
-          if (password) {
-            properties['ambari.dispatch.credential.password'] = password;
-          }
-          properties['mail.smtp.starttls.enable'] = startTls;
-        } else {
-          properties['mail.smtp.auth'] = false;
-        }
-      } else if (notificationType === 'SNMP' || notificationType === 'Custom SNMP') {
-        properties['ambari.dispatch.snmp.version'] = snmpVersion;
-        properties['ambari.dispatch.snmp.community'] = snmpCommunity;
-        properties['ambari.dispatch.snmp.host'] = snmpHosts;
-        properties['ambari.dispatch.snmp.port'] = snmpPort;
-        
-        if (notificationType === 'Custom SNMP') {
-          properties['ambari.dispatch.snmp.oid'] = snmpOid;
-        }
-      } else if (notificationType === 'Alert Script') {
-        properties['ambari.dispatch-property.script_file_name'] = scriptFilename;
-        properties['ambari.dispatch.script.filename'] = scriptFilename;
-        if (scriptDispatchProperty) {
-          properties['ambari.dispatch.script.parameter.1'] = scriptDispatchProperty;
-        }
-      }
-      
-      // Add custom properties
-      customProperties.forEach(prop => {
-        properties[prop.name] = prop.value;
-      });
-
-      // Prepare notification data
-      const notificationData: any = {
+      const form: AlertNotificationForm = {
         name: notificationName,
         description: notificationDescription,
-        notification_type: notificationType,
-        properties,
+        type: notificationType as AlertNotificationForm['type'],
         global: groupsOption === 'all',
-        alert_states: selectedSeverities.length > 0 ? selectedSeverities : ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'],
+        groups: selectedGroups,
+        alertStates: selectedSeverities,
+        recipients: notificationRecipients,
+        smtpHost: smtpServer,
+        smtpPort,
+        emailFrom,
+        useAuthentication,
+        username,
+        password,
+        passwordConfirmation,
+        startTls,
+        snmpVersion,
+        snmpCommunity,
+        snmpHosts,
+        snmpPort,
+        snmpOid,
+        scriptFilename,
+        scriptDispatchProperty,
+        customProperties,
+        existingProperties: selectedNotification?.AlertTarget.properties,
+        preserveSensitivePassword: notificationFormMode === 'edit',
       };
-
-      // Add groups if not global
-      if (!notificationData.global && selectedGroups.length > 0) {
-        // Use the group IDs directly
-        notificationData.groups = selectedGroups;
+      const errors = validateAlertNotificationForm(form);
+      const duplicateName = notifications.some((notification) =>
+        notification.AlertTarget.name.trim().toLowerCase() === notificationName.trim().toLowerCase() &&
+        (notificationFormMode !== 'edit' || notification.AlertTarget.id !== selectedNotification?.AlertTarget.id),
+      );
+      if (duplicateName) errors.unshift('An Alert Notification with this name already exists.');
+      if (errors.length > 0) {
+        setModalError(errors.join(' '));
+        return;
       }
-      
-      const requestData = {
-        AlertTarget: notificationData
-      };
-
-      if (notificationFormMode === 'create') {
-        // Create new notification
-        const response = await AlertsApi.createNotification(clusterName, requestData);
-        
-        if (response) {
-          // Immediately add the new notification to state for instant UI update
-          const newNotification: AlertNotification = {
-            AlertTarget: {
-              id: response.data?.AlertTarget?.id || Date.now(), // Use response ID or fallback
-              name: notificationName,
-              description: notificationDescription,
-              notification_type: notificationType,
-              properties,
-              global: groupsOption === 'all',
-              alert_states: selectedSeverities.length > 0 ? selectedSeverities : ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'],
-              groups: groupsOption === 'custom' ? selectedGroups : [],
-              is_enabled: true,
-              _isNew: true
-            }
-          };
-          
-          // Update notifications state immediately
-          const updatedNotifications = [...notifications, newNotification];
-          setNotifications(updatedNotifications);
-          
-          setModalSuccess('Notification created successfully');
-        }
-      } else if (notificationFormMode === 'edit' && selectedNotification) {
-        // Update existing notification
-        const notificationId = selectedNotification.AlertTarget.id;
-        if (notificationId) {
-          const updatedNotification = {
-            ...notificationData,
-            id: notificationId
-          };
-          
-          const response = await AlertsApi.updateNotification(clusterName, notificationId, { AlertTarget: updatedNotification });
-          if (response) {
-            // Update the notification in state immediately
-            const updatedNotifications = notifications.map(notification => 
-              notification.AlertTarget.id === notificationId 
-                ? { 
-                    AlertTarget: { 
-                      ...notification.AlertTarget,
-                      name: notificationName,
-                      description: notificationDescription,
-                      notification_type: notificationType,
-                      properties,
-                      global: groupsOption === 'all',
-                      alert_states: selectedSeverities.length > 0 ? selectedSeverities : ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'],
-                      groups: groupsOption === 'custom' ? selectedGroups : [],
-                      _isModified: true 
-                    } 
-                  }
-                : notification
-            );
-            setNotifications(updatedNotifications);
-            setSelectedNotification({ 
-              AlertTarget: { 
-                ...selectedNotification.AlertTarget,
-                name: notificationName,
-                description: notificationDescription,
-                notification_type: notificationType,
-                properties,
-                global: groupsOption === 'all',
-                alert_states: selectedSeverities.length > 0 ? selectedSeverities : ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'],
-                groups: groupsOption === 'custom' ? selectedGroups : [],
-                _isModified: true 
-              } 
-            });
-            setModalSuccess('Notification updated successfully');
-          }
-        }
-      } else if (notificationFormMode === 'duplicate') {
-        // Create a duplicate notification
-        const response = await AlertsApi.createNotification(clusterName, requestData);
-        
-        if (response) {
-          // Immediately add the duplicated notification to state
-          const newNotification: AlertNotification = {
-            AlertTarget: {
-              id: response.data?.AlertTarget?.id || Date.now(), // Use response ID or fallback
-              name: notificationName,
-              description: notificationDescription,
-              notification_type: notificationType,
-              properties,
-              global: groupsOption === 'all',
-              alert_states: selectedSeverities.length > 0 ? selectedSeverities : ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'],
-              groups: groupsOption === 'custom' ? selectedGroups : [],
-              is_enabled: true,
-              _isNew: true
-            }
-          };
-          
-          // Update notifications state immediately
-          const updatedNotifications = [...notifications, newNotification];
-          setNotifications(updatedNotifications);
-          
-          setModalSuccess('Notification duplicated successfully');
-        }
+      setLoading(true);
+      const payload = buildAlertNotificationPayload(form);
+      if (notificationFormMode === 'edit') {
+        const notificationId = selectedNotification?.AlertTarget.id;
+        if (!notificationId) throw new Error('The selected notification has no server ID.');
+        await AlertsApi.updateNotification(clusterName, notificationId, payload);
+      } else {
+        await AlertsApi.createNotification(clusterName, payload);
       }
 
-      // Reset form
+      await loadNotifications();
+      setSelectedNotification(null);
       resetNotificationForm();
       setShowNotificationFormModal(false);
-      
-      // Reload notifications in the background to ensure consistency
-      setTimeout(() => {
-        loadNotifications();
-      }, 1000);
+      setModalSuccess(notificationFormMode === 'edit'
+        ? 'Notification updated successfully.'
+        : notificationFormMode === 'duplicate'
+          ? 'Notification duplicated successfully.'
+          : 'Notification created successfully.');
     } catch (error) {
       console.error('Error submitting notification form:', error);
       setModalError('Failed to save notification. Please try again.');
@@ -2245,8 +2046,6 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
 
   // Handle rename group
   const handleRenameGroup = (group: AlertGroupItem | AlertGroupState): void => {
-  // ...existing code...
-    
     setGroupToRename(group);
     const currentName = getGroupName(group);
     setRenameGroupName(currentName);
@@ -2258,8 +2057,6 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
 
   // Handle duplicate group
   const handleDuplicateGroup = (group: AlertGroupItem | AlertGroupState): void => {
-  // ...existing code...
-    
     setGroupToDuplicate(group);
     
     // Set initial name for the duplicated group
@@ -2284,183 +2081,29 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
   };
 
   // Handle notifications change for a specific group
-  const handleNotificationsChange = async (groupId: number, updatedNotifications: AlertNotification[]): Promise<void> => {
-  // ...existing code...
-    
-    try {
-      setModalError('');
-      
-      // Find the current notifications for this group
-      const currentGroupNotifications = notifications.filter(notification => {
-        const target = notification.AlertTarget;
-        if (target.global) return true;
-        if (target.groups && Array.isArray(target.groups)) {
-          return target.groups.includes(groupId);
-        }
-        return false;
-      });
-      
-  // ...existing code...
-      
-      // Find notifications to add and remove
-      const notificationsToAdd = updatedNotifications.filter(updated => 
-        !currentGroupNotifications.some(current => current.AlertTarget.id === updated.AlertTarget.id)
-      );
-      
-      const notificationsToRemove = currentGroupNotifications.filter(current => 
-        !updatedNotifications.some(updated => updated.AlertTarget.id === current.AlertTarget.id) &&
-        !current.AlertTarget.global // Don't remove global notifications
-      );
-      
-  // ...existing code...
-      
-      // Update the notifications state immediately to reflect changes in UI
-      const updatedNotificationsState = notifications.map(notification => {
-        const target = notification.AlertTarget;
-        
-        // For notifications being added to this group
-        if (notificationsToAdd.some(toAdd => toAdd.AlertTarget.id === target.id)) {
-          if (!target.global && target.id) {
-            const currentGroups = target.groups || [];
-            const updatedGroups = [...currentGroups, groupId];
-            return {
-              ...notification,
-              AlertTarget: {
-                ...target,
-                groups: updatedGroups
-              }
-            };
-          }
-        }
-        
-        // For notifications being removed from this group
-        if (notificationsToRemove.some(toRemove => toRemove.AlertTarget.id === target.id)) {
-          if (!target.global && target.id) {
-            const currentGroups = target.groups || [];
-            const updatedGroups = currentGroups.filter(id => id !== groupId);
-            return {
-              ...notification,
-              AlertTarget: {
-                ...target,
-                groups: updatedGroups
-              }
-            };
-          }
-        }
-        
-        return notification;
-      });
-      
-  // ...existing code...
-      
-      // Update the notifications state immediately
-      setNotifications(updatedNotificationsState);
-      
-      // Make API calls in the background
-      setLoading(true);
-      
-      // Update notifications by modifying their group assignments
-      for (const notification of notificationsToAdd) {
-        if (!notification.AlertTarget.global && notification.AlertTarget.id) {
-          const currentGroups = notification.AlertTarget.groups || [];
-          const updatedGroups = [...currentGroups, groupId];
-          
-          // ...existing code...
-          
-          await AlertsApi.updateNotification(clusterName, notification.AlertTarget.id, {
-            AlertTarget: {
-              groups: updatedGroups
-            }
-          });
-        }
-      }
-      
-      for (const notification of notificationsToRemove) {
-        if (!notification.AlertTarget.global && notification.AlertTarget.id) {
-          const currentGroups = notification.AlertTarget.groups || [];
-          const updatedGroups = currentGroups.filter(id => id !== groupId);
-          
-          // ...existing code...
-          
-          await AlertsApi.updateNotification(clusterName, notification.AlertTarget.id, {
-            AlertTarget: {
-              groups: updatedGroups
-            }
-          });
-        }
-      }
-      
-      // Mark the selected group as modified to enable the save button
-      if (selectedGroup) {
-        if ('AlertGroup' in selectedGroup) {
-          // Update AlertGroupItem
-          const updatedGroup = {
-            ...selectedGroup,
-            AlertGroup: {
-              ...selectedGroup.AlertGroup,
-              _isModified: true
-            }
-          };
-          
-          // Update the group in the alertGroups array
-          const updatedGroups = alertGroups.map(g => 
-            g.AlertGroup.id === selectedGroup.AlertGroup.id ? updatedGroup : g
-          );
-          
-          setAlertGroups(updatedGroups);
-          setSelectedGroup(updatedGroup);
-        } else {
-          // Update AlertGroupState
-          const updatedGroup: AlertGroupState = {
-            ...selectedGroup,
-            _isModified: true
-          };
-          
-          setSelectedGroup(updatedGroup);
-          
-          // Also update the corresponding group in alertGroups
-          const updatedGroups = alertGroups.map(g => {
-            if (g.AlertGroup.id === selectedGroup.id) {
-              return {
-                ...g,
-                AlertGroup: {
-                  ...g.AlertGroup,
-                  _isModified: true
-                }
-              };
-            }
-            return g;
-          });
-          
-          setAlertGroups(updatedGroups);
+  const handleNotificationsChange = (groupId: number, updatedNotifications: AlertNotification[]): void => {
+    const targets = updatedNotifications.flatMap((notification) =>
+      notification.AlertTarget.id ? [notification.AlertTarget] : [],
+    );
+    const updatedGroups = alertGroups.map((group) => group.AlertGroup.id === groupId
+      ? {
+          ...group,
+          AlertGroup: {
+            ...group.AlertGroup,
+            targets,
+            _isModified: !group.AlertGroup._isNew,
+          },
         }
-        
-        // Enable save button
-        setIsSaveDisabled(false);
-        
-        setModalSuccess('Notification assignments updated successfully');
-        
-        setTimeout(() => {
-          setModalSuccess('');
-        }, 5000);
-      }
-    } catch (error) {
-      console.error('Error updating notification assignments:', error);
-      setModalError('Failed to update notification assignments');
-      
-      setTimeout(() => {
-        setModalError('');
-      }, 5000);
-    } finally {
-      setLoading(false);
-    }
+      : group,
+    );
+    setAlertGroups(updatedGroups);
+    const updatedGroup = updatedGroups.find((group) => group.AlertGroup.id === groupId);
+    if (updatedGroup) handleSelectGroup(updatedGroup);
+    setIsSaveDisabled(false);
+    setModalError('');
+    setModalSuccess('Notification assignments changed. Click Save to apply them.');
   };
 
-  // Hide the entire ActionsButton when upgrade is blocking
-  if (isUpgradeBlocking) {
-    return null;
-  }
-
   return (
     <>
       <Modal
@@ -2493,6 +2136,16 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
           ACTIONS&nbsp; <span className="caret"></span>
         </button>
         <ul className={`dropdown-menu dropdown-menu-end ${isActionsDropdownOpen ? 'show' : ''}`} style={{ top: '100%', marginTop: '2px' }}>
+          {actionPolicy.create && (
+            <li>
+              <button
+                className="dropdown-item"
+                onClick={() => navigate('/main/alerts/add/1')}
+              >
+                <FontAwesomeIcon icon={faPlus} />&nbsp; Create Alert
+              </button>
+            </li>
+          )}
           <li>
             <button 
               className="dropdown-item" 
@@ -2502,7 +2155,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
             </button>
           </li>
           {/* Manage Notifications - Requires CLUSTER.MANAGE_ALERT_NOTIFICATIONS authorization */}
-          {canManageNotifications && (
+          {actionPolicy.notifications && (
             <li>
               <button 
                 className="dropdown-item" 
@@ -2658,7 +2311,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
         isOpen={showDeleteNotificationModal}
         onClose={handleCloseModal}
         modalTitle="Confirm Delete"
-        modalBody={<DeleteNotificationModal notification={selectedNotification} />}
+        modalBody={<DeleteNotificationModal notification={selectedNotification} errorMessage={modalError} />}
         successCallback={() => selectedNotification && handleDeleteNotification(selectedNotification)}
         options={{
           modalSize: 'modal-sm',
@@ -2742,6 +2395,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
             successMessage={modalSuccess}
             setErrorMessage={setModalError}
             setSuccessMessage={setModalSuccess}
+            preserveSensitivePassword={notificationFormMode === 'edit'}
           />
         }
         successCallback={handleNotificationFormSubmit}
@@ -2753,7 +2407,7 @@ export const ActionsButton: React.FC<ActionsButtonProps> = ({
           cancelableViaIcon: true,
           cancelableViaBtn: true,
           okButtonVariant: 'primary',
-          okButtonDisabled: !notificationName.trim() || !isNotificationNameValid || modalError !== '',
+          okButtonDisabled: !notificationName.trim() || !isNotificationNameValid,
         }}
       />
 
diff --git a/ambari-web/latest/src/screens/Alerts/AlertConfigSection.tsx b/ambari-web/latest/src/screens/Alerts/AlertConfigSection.tsx
index 661cc1aa62..427583c271 100644
--- a/ambari-web/latest/src/screens/Alerts/AlertConfigSection.tsx
+++ b/ambari-web/latest/src/screens/Alerts/AlertConfigSection.tsx
@@ -16,16 +16,22 @@
  * limitations under the License.
  */
 
-import React, { useState, useEffect, useContext } from 'react';
+import React, { forwardRef, useContext, useEffect, useImperativeHandle, useState } from 'react';
 import { useParams } from 'react-router-dom';
 import { AlertsApi } from '../../api/alertsApi';
 import { Form, Button, Row, Col, InputGroup } from 'react-bootstrap';
 import "../../styles/app.scss"
 import { useAuth } from '../../hooks/useAuth';
 import { AppContext } from '../../store/context';
+import {
+    buildAlertDefinitionUpdate,
+    validateAlertDefinitionConfiguration,
+} from '../../Utils/alertDefinitions';
+import { AlertEditorHandle } from './types';
 
 interface AlertConfigSectionProps {
     clusterName: string;
+    onDirtyChange?: (dirty: boolean) => void;
 }
 
 interface AlertParameter {
@@ -51,6 +57,7 @@ interface ReportingType {
 }
 
 interface AlertSourceConfig {
+    [key: string]: unknown;
     parameters?: AlertParameter[];
     reporting?: ReportingType;
     path?: string;
@@ -67,11 +74,13 @@ interface AlertDefinitionConfig {
     [key: string]: any;
 }
 
-const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
+const AlertConfigSection = forwardRef<AlertEditorHandle, AlertConfigSectionProps>(({
+    clusterName,
+    onDirtyChange,
+}, ref) => {
     const { alertId } = useParams<{ alertId: string }>();
     const { upgradeIsRunning, upgradeSuspended } = useContext(AppContext);
     const [canEdit, setCanEdit] = useState(false);
-    const [hasErrors, setHasErrors] = useState(false);
     const [configurations, setConfigurations] = useState<AlertDefinitionConfig>({
         id: 0,
         description: '',
@@ -85,6 +94,7 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
     const [loading, setLoading] = useState(true);
     const [isSaving, setIsSaving] = useState(false);
     const [errorMessage, setErrorMessage] = useState('');
+    const [retryTrigger, setRetryTrigger] = useState(0);
     
     // Authorization hooks - implementing Ember.js alert authorization patterns
     const { hasAuthorization } = useAuth();
@@ -97,29 +107,18 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
 
     useEffect(() => {
         const fetchAlertDefinition = async () => {
+            setLoading(true);
+            setErrorMessage('');
             try {
-                const time = new Date().getTime();
-                const data = await AlertsApi.getAlertDefinition(clusterName, 'AlertDefinition/component_name,AlertDefinition/description,AlertDefinition/enabled,AlertDefinition/repeat_tolerance,AlertDefinition/repeat_tolerance_enabled,AlertDefinition/id,AlertDefinition/ignore_host,AlertDefinition/interval,AlertDefinition/label,AlertDefinition/name,AlertDefinition/scope,AlertDefinition/service_name,AlertDefinition/source,AlertDefinition/help_url', time);
-                if (data.items && data.items.length > 0) {
-                    const alertDefinition = data.items.find((item: any) => 
-                        item.AlertDefinition.id === parseInt(alertId || '0')
-                    );
-                    
-                    if (alertDefinition) {
-                        setConfigurations(alertDefinition.AlertDefinition);
-                        setOriginalConfigurations(JSON.parse(JSON.stringify(alertDefinition.AlertDefinition)));
-                    } else {
-                        console.error('Alert definition not found');
-                        setErrorMessage('Alert definition not found');
-                    }
-                } else {
-                    console.error('No alert definitions found');
-                    setErrorMessage('No alert definitions found');
-                }
-                setLoading(false);
+                const data = await AlertsApi.getAlertDefinitionById(clusterName, alertId || '', Date.now());
+                const definition = data?.AlertDefinition || data?.items?.[0]?.AlertDefinition;
+                if (!definition) throw new Error('Alert definition not found');
+                setConfigurations(definition);
+                setOriginalConfigurations(JSON.parse(JSON.stringify(definition)));
             } catch (error) {
                 console.error('Error fetching alert definition:', error);
-                setErrorMessage('Error fetching alert definition');
+                setErrorMessage('Failed to load alert configuration.');
+            } finally {
                 setLoading(false);
             }
         };
@@ -127,83 +126,37 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
         if (clusterName && alertId) {
             fetchAlertDefinition();
         }
-    }, [clusterName, alertId]);
+    }, [clusterName, alertId, retryTrigger]);
 
     const handleEditConfigs = () => {
         setCanEdit(true);
+        setErrorMessage('');
+        onDirtyChange?.(true);
     };
 
     const handleCancelEditConfigs = () => {
         // Reset to original values
         setConfigurations(JSON.parse(JSON.stringify(originalConfigurations)));
         setCanEdit(false);
-        setHasErrors(false);
         setErrorMessage('');
+        onDirtyChange?.(false);
     };
 
-    const handleSaveConfigs = async () => {
-        if (hasErrors) {
-            return;
+    const validationErrors = validateAlertDefinitionConfiguration(configurations);
+
+    const handleSaveConfigs = async (): Promise<boolean> => {
+        if (validationErrors.length > 0) {
+            setErrorMessage(validationErrors.join(' '));
+            return false;
         }
         
         setIsSaving(true);
         
         try {
-            // Prepare the payload for the API call
-            const payload: Record<string, any> = {};
-            
-            // Add interval if it has changed
-            if (configurations.interval !== originalConfigurations.interval) {
-                payload["AlertDefinition/interval"] = configurations.interval.toString();
-            }
-            
-            // Add source if it has changed
-            if (configurations.source) {
-                const originalSource = originalConfigurations.source || {};
-                const currentSource = configurations.source;
-                
-                // Deep comparison to check if source has changed
-                const sourceChanged = JSON.stringify(currentSource) !== JSON.stringify(originalSource);
-                
-                if (sourceChanged) {
-                    // Create a complete source object for the payload
-                    const sourcePayload: any = {
-                        type: currentSource.type
-                    };
-                    
-                    // Add parameters if they exist
-                    if (currentSource.parameters && currentSource.parameters.length > 0) {
-                        sourcePayload.parameters = currentSource.parameters;
-                    }
-                    
-                    // Add reporting if it exists
-                    if (currentSource.reporting) {
-                        sourcePayload.reporting = currentSource.reporting;
-                    }
-                    
-                    // Add path if it exists
-                    if (currentSource.path) {
-                        sourcePayload.path = currentSource.path;
-                    }
-                    
-                    // Add default_port if it exists
-                    if (currentSource.default_port) {
-                        sourcePayload.default_port = currentSource.default_port;
-                    }
-                    
-                    // Add uri if it exists
-                    if (currentSource.uri) {
-                        sourcePayload.uri = currentSource.uri;
-                    }
-                    
-                    payload["AlertDefinition/source"] = sourcePayload;
-                }
-            }
+            const payload = buildAlertDefinitionUpdate(configurations, originalConfigurations);
             
             // Only make the API call if there are changes
             if (Object.keys(payload).length > 0) {
-                console.log('Saving configuration with payload:', payload);
-                
                 await AlertsApi.updateAlertDefinition(
                     clusterName,
                     configurations.id,
@@ -217,15 +170,23 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
                 // No changes were made
                 setCanEdit(false);
             }
+            setErrorMessage('');
+            onDirtyChange?.(false);
+            return true;
         } catch (error) {
             console.error('Error saving configurations:', error);
             setErrorMessage('Error saving configurations. Please try again.');
-            setHasErrors(true);
+            return false;
         } finally {
             setIsSaving(false);
         }
     };
 
+    useImperativeHandle(ref, () => ({
+        save: handleSaveConfigs,
+        discard: handleCancelEditConfigs,
+    }));
+
     const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, key: string) => {
         const value = e.target.value;
         
@@ -239,18 +200,20 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
                 ...prev,
                 description: value
             }));
-        } else if (configurations.source?.parameters) {
+        } else if (configurations.source) {
             // Check if this is a parameter change
-            const paramIndex = configurations.source.parameters.findIndex(param => param.name === key);
+            const paramIndex = (configurations.source.parameters || []).findIndex(param => param.name === key);
             
             if (paramIndex !== -1) {
                 // Handle parameter changes
-                const updatedParams = [...configurations.source.parameters];
+                const updatedParams = [...(configurations.source.parameters || [])];
                 const param = updatedParams[paramIndex];
                 
                 updatedParams[paramIndex] = {
                     ...param,
-                    value: param.type === 'NUMERIC' ? (parseInt(value) || 0) : value
+                    value: param.type === 'NUMERIC' || param.type === 'PERCENT'
+                        ? (value === '' ? '' : Number(value))
+                        : value
                 };
                 
                 setConfigurations(prev => ({
@@ -268,7 +231,7 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
                 if (currentReporting[status] && typeof currentReporting[status] === 'object') {
                     const reportingItem = { 
                         ...(currentReporting[status] as AlertReportingItem),
-                        value: parseFloat(value) || 0
+                        value: value === '' ? 0 : Number(value)
                     };
                     
                     setConfigurations(prev => ({
@@ -313,6 +276,15 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
         return <div>Loading...</div>;
     }
 
+    if (!configurations.id && errorMessage) {
+        return (
+            <div className="col col-md-8">
+                <div className="alert alert-danger">{errorMessage}</div>
+                <Button variant="primary" onClick={() => setRetryTrigger((value) => value + 1)}>Retry</Button>
+            </div>
+        );
+    }
+
 
     const fieldsToShow: Record<string, string> = {
         description: "Description",
@@ -407,12 +379,12 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
                                         )}
                                         <InputGroup>
                                             <Form.Control
-                                                type={param.type === 'NUMERIC' ? 'number' : 'text'}
+                                                type={param.type === 'NUMERIC' || param.type === 'PERCENT' ? 'number' : 'text'}
                                                 value={param.value}
                                                 onChange={(e) => handleChange(e, param.name)}
                                                 aria-label={param.display_name}
                                                 aria-describedby={`basic-addon-${param.name}`}
-                                                disabled={!canEdit || isSaving}
+                                                disabled={!canEdit || isSaving || param.visibility === 'READ_ONLY'}
                                             />
                                             {param.units && (
                                                 <InputGroup.Text id={`basic-addon-${param.name}`}>{param.units}</InputGroup.Text>
@@ -491,7 +463,7 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
                             <Button
                                 variant="primary"
                                 onClick={handleSaveConfigs}
-                                disabled={hasErrors || isSaving}
+                                disabled={validationErrors.length > 0 || isSaving}
                                 style={{width: '100px', height: '40px'}}
                             >
                                 {isSaving ? 'Saving...' : 'Save'}
@@ -502,6 +474,8 @@ const AlertConfigSection = ({ clusterName }: AlertConfigSectionProps) => {
             </div>
         </div>
     );
-};
+});
+
+AlertConfigSection.displayName = 'AlertConfigSection';
 
 export default AlertConfigSection;
diff --git a/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx b/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx
index 2f04813ff0..8f6d6719ad 100644
--- a/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx
+++ b/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx
@@ -17,28 +17,19 @@
  */
 
 // ambari-web/ui2/src/screens/Alerts/AlertDefinitionDetails.tsx
-import { useState, useEffect, useContext } from 'react';
+import { useContext, useEffect, useRef, useState } from 'react';
 import Spinner from '../../components/Spinner';
 import HeaderSection from './HeaderSection';
 import AlertConfigSection from './AlertConfigSection';
 import InformationSection from './InformationSection';
 import AlertInstancesTable from './AlertInstancesTable';
 import { Container } from "react-bootstrap";
-import { useParams } from "react-router-dom";
+import { useBlocker, useParams } from "react-router-dom";
 import { AlertsApi } from "../../api/alertsApi";
 import { AppContext } from "../../store/context";
-import { processData } from './alertUtils';
-import { AlertStatusObject, MergedAlert } from './types';
-
-interface AlertDefinitionResponseItem {
-    AlertDefinition: {
-        id: number;
-        repeat_tolerance: any;
-        repeat_tolerance_enabled: boolean;
-        help_url?: string;
-        [key: string]: any;
-    };
-}
+import { AlertEditorHandle, AlertStatusObject, MergedAlert } from './types';
+import { buildAlertDefinitionDetails } from '../../Utils/alertDefinitions';
+import Modal from '../../components/Modal';
 
 const AlertDefinitionDetails = () => {
     const { clusterName } = useContext(AppContext);
@@ -48,6 +39,17 @@ const AlertDefinitionDetails = () => {
     const [statuses, setStatuses] = useState<AlertStatusObject[]>([]);
     const [errorMessage, setErrorMessage] = useState('');
     const [refreshTrigger, setRefreshTrigger] = useState(0);
+    const [retryTrigger, setRetryTrigger] = useState(0);
+    const [labelDirty, setLabelDirty] = useState(false);
+    const [configDirty, setConfigDirty] = useState(false);
+    const [isSavingBeforeLeave, setIsSavingBeforeLeave] = useState(false);
+    const [leaveError, setLeaveError] = useState('');
+    const labelEditorRef = useRef<AlertEditorHandle>(null);
+    const configEditorRef = useRef<AlertEditorHandle>(null);
+    const hasUnsavedChanges = labelDirty || configDirty;
+    const blocker = useBlocker(({ currentLocation, nextLocation }) =>
+        hasUnsavedChanges && currentLocation.pathname !== nextLocation.pathname,
+    );
 
     const handleAlertDefinitionUpdate = (updatedFields: Partial<MergedAlert>) => {
         if (alertDefinition) {
@@ -57,54 +59,85 @@ const AlertDefinitionDetails = () => {
         }
     };
 
+    useEffect(() => {
+        if (!hasUnsavedChanges) return;
+        const handleBeforeUnload = (event: BeforeUnloadEvent) => {
+            event.preventDefault();
+            event.returnValue = '';
+        };
+        window.addEventListener('beforeunload', handleBeforeUnload);
+        return () => window.removeEventListener('beforeunload', handleBeforeUnload);
+    }, [hasUnsavedChanges]);
+
+    const handleSaveAndLeave = async () => {
+        if (blocker.state !== 'blocked') return;
+        setIsSavingBeforeLeave(true);
+        setLeaveError('');
+        const results = await Promise.all([
+            labelDirty ? labelEditorRef.current?.save() ?? Promise.resolve(false) : Promise.resolve(true),
+            configDirty ? configEditorRef.current?.save() ?? Promise.resolve(false) : Promise.resolve(true),
+        ]);
+        setIsSavingBeforeLeave(false);
+        if (results.every(Boolean)) {
+            blocker.proceed();
+        } else {
+            setLeaveError('One or more changes could not be saved. Correct the errors and try again, or discard the changes.');
+        }
+    };
+
+    const handleDiscardAndLeave = () => {
+        if (blocker.state !== 'blocked') return;
+        labelEditorRef.current?.discard();
+        configEditorRef.current?.discard();
+        setLeaveError('');
+        blocker.proceed();
+    };
+
+    const handleCancelLeave = () => {
+        if (blocker.state !== 'blocked') return;
+        setLeaveError('');
+        blocker.reset();
+    };
+
     useEffect(() => {
         const fetchAlertDetails = async () => {
+            setIsLoaded(false);
+            setErrorMessage('');
             try {
                 const alertDefinitionId = params.alertId;
                 if (alertDefinitionId) {
-                    const [alertsResponse, summariesResponse, alertDefinitionResponse] = await Promise.all([
+                    const [groupsResponse, summariesResponse, alertDefinitionResponse] = await Promise.all([
                         AlertsApi.getAlerts(
                             clusterName,
                             'AlertGroup/default,AlertGroup/definitions,AlertGroup/id,AlertGroup/name,AlertGroup/targets',
                             Date.now()
                         ),
                         AlertsApi.getGroupFormattedAlertsNotifications(clusterName, Date.now()),
-                        AlertsApi.getAlertDefinition(
-                            clusterName,
-                            'AlertDefinition/component_name,AlertDefinition/description,AlertDefinition/enabled,AlertDefinition/repeat_tolerance,AlertDefinition/repeat_tolerance_enabled,AlertDefinition/id,AlertDefinition/ignore_host,AlertDefinition/interval,AlertDefinition/label,AlertDefinition/name,AlertDefinition/scope,AlertDefinition/service_name,AlertDefinition/source,AlertDefinition/help_url',
-                            Date.now()
-                        )
+                        AlertsApi.getAlertDefinitionById(clusterName, alertDefinitionId, Date.now())
                     ]);
-
-                    const processedAlerts = processData(alertsResponse, summariesResponse);
-                    const alertDetails = processedAlerts.find(alert => alert.alert_definition_id === parseInt(alertDefinitionId));
-                    const alertDefinitionDetails = alertDefinitionResponse.items.find((item: AlertDefinitionResponseItem) => 
-                        item.AlertDefinition.id === parseInt(alertDefinitionId)
+                    const definition = alertDefinitionResponse?.AlertDefinition ||
+                        alertDefinitionResponse?.items?.[0]?.AlertDefinition;
+                    if (!definition) throw new Error('Alert definition not found');
+                    const details = buildAlertDefinitionDetails(
+                        definition,
+                        groupsResponse?.items || [],
+                        summariesResponse?.alerts_summary_grouped || [],
                     );
-
-                    if (alertDetails && alertDefinitionDetails) {
-                        setAlertDefinition({
-                            ...alertDetails,
-                            repeat_tolerance: alertDefinitionDetails.AlertDefinition.repeat_tolerance || 1,
-                            repeat_tolerance_enabled: alertDefinitionDetails.AlertDefinition.repeat_tolerance_enabled || false,
-                            help_url: alertDefinitionDetails.AlertDefinition.help_url
-                        });
-                        setStatuses(alertDetails.statuses);
-                    } else {
-                        setErrorMessage('Alert details not found');
-                    }
+                    setAlertDefinition(details);
+                    setStatuses(details.statuses);
                 } else {
                     setErrorMessage('Alert ID is missing');
                 }
-                setIsLoaded(true);
             } catch (error) {
                 setErrorMessage('Failed to fetch alert definition details');
+                setAlertDefinition(null);
+            } finally {
                 setIsLoaded(true);
             }
         };
 
         fetchAlertDetails();
-    }, [params, clusterName]);
+    }, [params.alertId, clusterName, retryTrigger]);
 
     if (!isLoaded) {
         return <Spinner />;
@@ -114,16 +147,30 @@ const AlertDefinitionDetails = () => {
         return (
             <Container className="p-4">
                 <div className="alert alert-danger">{errorMessage || 'Alert definition not found'}</div>
+                <button className="btn btn-primary" onClick={() => setRetryTrigger((value) => value + 1)}>
+                    Retry
+                </button>
             </Container>
         );
     }
 
     return (
+        <>
         <Container className="p-4">
             <div id="alert-definition-details">
-                <HeaderSection alertDefinition={alertDefinition} statuses={statuses} />
+                <HeaderSection
+                    ref={labelEditorRef}
+                    alertDefinition={alertDefinition}
+                    statuses={statuses}
+                    onDirtyChange={setLabelDirty}
+                    onAlertDefinitionUpdate={handleAlertDefinitionUpdate}
+                />
                 <div className="row gap-3">
-                    <AlertConfigSection clusterName={clusterName} />
+                    <AlertConfigSection
+                        ref={configEditorRef}
+                        clusterName={clusterName}
+                        onDirtyChange={setConfigDirty}
+                    />
                     <InformationSection 
                         alertDefinition={alertDefinition} 
                         onAlertDefinitionUpdate={handleAlertDefinitionUpdate}
@@ -133,12 +180,39 @@ const AlertDefinitionDetails = () => {
                     <AlertInstancesTable 
                         clusterName={clusterName} 
                         alert_id={params.alertId || ''} 
+                        definitionName={alertDefinition.name}
                         refreshTrigger={refreshTrigger}
                         alertEnabled={alertDefinition?.enabled ?? true}
                     />
                 </div>
             </div>
         </Container>
+        <Modal
+            isOpen={blocker.state === 'blocked'}
+            onClose={handleCancelLeave}
+            modalTitle="Unsaved Alert Definition Changes"
+            modalBody={
+                <div>
+                    <p>Save the current label and configuration changes before leaving this page?</p>
+                    {leaveError && <div className="alert alert-danger">{leaveError}</div>}
+                </div>
+            }
+            successCallback={handleSaveAndLeave}
+            options={{
+                okButtonText: isSavingBeforeLeave ? 'Saving...' : 'Save and Leave',
+                cancelButtonText: 'Cancel',
+                cancelableViaIcon: !isSavingBeforeLeave,
+                cancelableViaBtn: true,
+                okButtonDisabled: isSavingBeforeLeave,
+                extraButtons: [{
+                    text: 'Discard and Leave',
+                    onClick: handleDiscardAndLeave,
+                    variant: 'danger',
+                    disabled: isSavingBeforeLeave,
+                }],
+            }}
+        />
+        </>
     );
 };
 
diff --git a/ambari-web/latest/src/screens/Alerts/AlertDefinitionWizard.tsx b/ambari-web/latest/src/screens/Alerts/AlertDefinitionWizard.tsx
new file mode 100644
index 0000000000..83e1ca7689
--- /dev/null
+++ b/ambari-web/latest/src/screens/Alerts/AlertDefinitionWizard.tsx
@@ -0,0 +1,320 @@
+/**
+ * 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 { useContext, useEffect, useRef, useState } from "react";
+import { Alert, Button, Col, Container, Form, Row, Spinner } from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faFileCode, faGlobe, faLayerGroup, faPlug } from "@fortawesome/free-solid-svg-icons";
+import { useNavigate, useParams } from "react-router-dom";
+import { AlertsApi } from "../../api/alertsApi";
+import {
+  buildAlertCreationPayload,
+  INITIAL_ALERT_CREATION_FORM,
+  validateAlertCreation,
+  type AlertCreationForm,
+  type CreatableAlertType,
+} from "../../Utils/alertCreation";
+import { AppContext } from "../../store/context";
+
+const TYPE_OPTIONS: Array<{
+  type: CreatableAlertType;
+  label: string;
+  icon: typeof faPlug;
+}> = [
+  { type: "PORT", label: "Port", icon: faPlug },
+  { type: "WEB", label: "Web", icon: faGlobe },
+  { type: "SCRIPT", label: "Script", icon: faFileCode },
+  { type: "AGGREGATE", label: "Aggregate", icon: faLayerGroup },
+];
+
+function parseStep(value: string | undefined): number {
+  const step = Number((value || "1").replace(/^step/, ""));
+  return step >= 1 && step <= 3 ? step : 1;
+}
+
+const AlertDefinitionWizard = () => {
+  const { clusterName, services, serviceComponentInfo } = useContext(AppContext);
+  const { stepNumber } = useParams<{ stepNumber: string }>();
+  const navigate = useNavigate();
+  const step = parseStep(stepNumber);
+  const [form, setForm] = useState<AlertCreationForm>({ ...INITIAL_ALERT_CREATION_FORM });
+  const [aggregateDefinitions, setAggregateDefinitions] = useState<string[]>([]);
+  const [errors, setErrors] = useState<string[]>([]);
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const submitInFlight = useRef(false);
+
+  const installedServices: string[] = services
+    .map((service: any) => service?.ServiceInfo?.service_name)
+    .filter((name: unknown): name is string => typeof name === "string")
+    .sort();
+  const serviceNames: string[] = [...new Set([...installedServices, "CUSTOM"] as string[])];
+  const stackService = serviceComponentInfo?.items?.find(
+    (service: any) => service?.StackServices?.service_name === form.serviceName,
+  );
+  const components: string[] = (stackService?.components || [])
+    .map((component: any) => component?.StackServiceComponents?.component_name)
+    .filter((name: unknown): name is string => typeof name === "string")
+    .sort();
+
+  useEffect(() => {
+    if (step > 1 && !form.type) {
+      navigate("/main/alerts/add/1", { replace: true });
+    } else if (step === 3 && validateAlertCreation(form).length > 0) {
+      navigate("/main/alerts/add/2", { replace: true });
+    }
+  }, [form, navigate, step]);
+
+  useEffect(() => {
+    if (!clusterName) return;
+    AlertsApi.getAlertDefinition(clusterName, "AlertDefinition/name,AlertDefinition/source", Date.now())
+      .then((response) => {
+        setAggregateDefinitions((response.items || [])
+          .map((item: any) => item?.AlertDefinition?.name)
+          .filter((name: unknown): name is string => typeof name === "string")
+          .sort());
+      })
+      .catch(() => setAggregateDefinitions([]));
+  }, [clusterName]);
+
+  const update = <K extends keyof AlertCreationForm>(key: K, value: AlertCreationForm[K]) => {
+    setForm((current) => ({ ...current, [key]: value }));
+    setErrors([]);
+  };
+
+  const chooseType = (type: CreatableAlertType) => {
+    setForm((current) => ({ ...current, type }));
+    navigate("/main/alerts/add/2");
+  };
+
+  const continueToReview = () => {
+    const validationErrors = validateAlertCreation(form);
+    setErrors(validationErrors);
+    if (validationErrors.length === 0) navigate("/main/alerts/add/3");
+  };
+
+  const submit = async () => {
+    if (submitInFlight.current) return;
+    const validationErrors = validateAlertCreation(form);
+    setErrors(validationErrors);
+    if (validationErrors.length > 0) return;
+    if (!clusterName) {
+      setErrors(["Cluster context is unavailable. Return to Alerts and retry."]);
+      return;
+    }
+    submitInFlight.current = true;
+    setIsSubmitting(true);
+    try {
+      await AlertsApi.createAlertDefinition(clusterName, buildAlertCreationPayload(form));
+      navigate("/main/alerts");
+    } catch {
+      setErrors(["Ambari could not create the Alert Definition. Review the values and retry."]);
+    } finally {
+      submitInFlight.current = false;
+      setIsSubmitting(false);
+    }
+  };
+
+  const commonFields = (
+    <>
+      <Row className="g-3">
+        <Col md={6}>
+          <Form.Group controlId="alert-name">
+            <Form.Label>Alert Name</Form.Label>
+            <Form.Control value={form.label} maxLength={255} onChange={(event) => update("label", event.target.value)} />
+          </Form.Group>
+        </Col>
+        <Col md={6}>
+          <Form.Group controlId="alert-service">
+            <Form.Label>Service</Form.Label>
+            <Form.Select value={form.serviceName} onChange={(event) => {
+              update("serviceName", event.target.value);
+              update("componentName", "");
+            }}>
+              <option value="">Select service</option>
+              {serviceNames.map((name) => <option key={name} value={name}>{name}</option>)}
+            </Form.Select>
+          </Form.Group>
+        </Col>
+        <Col md={6}>
+          <Form.Group controlId="alert-component">
+            <Form.Label>Component</Form.Label>
+            <Form.Select
+              value={form.componentName}
+              disabled={form.serviceName === "CUSTOM"}
+              onChange={(event) => update("componentName", event.target.value)}
+            >
+              <option value="">No component</option>
+              {components.map((name) => <option key={name} value={name}>{name}</option>)}
+            </Form.Select>
+          </Form.Group>
+        </Col>
+        <Col md={3}>
+          <Form.Group controlId="alert-scope">
+            <Form.Label>Scope</Form.Label>
+            <Form.Select value={form.scope} onChange={(event) => update("scope", event.target.value as AlertCreationForm["scope"])}>
+              <option value="ANY">Any</option>
+              <option value="HOST">Host</option>
+              <option value="SERVICE">Service</option>
+            </Form.Select>
+          </Form.Group>
+        </Col>
+        <Col md={3}>
+          <Form.Group controlId="alert-interval">
+            <Form.Label>Check Interval</Form.Label>
+            <Form.Control type="number" min={1} value={form.interval} onChange={(event) => update("interval", event.target.value)} />
+          </Form.Group>
+        </Col>
+        <Col xs={12}>
+          <Form.Group controlId="alert-description">
+            <Form.Label>Description</Form.Label>
+            <Form.Control as="textarea" rows={3} value={form.description} onChange={(event) => update("description", event.target.value)} />
+          </Form.Group>
+        </Col>
+      </Row>
+    </>
+  );
+
+  return (
+    <Container className="p-4">
+      <div className="d-flex justify-content-between align-items-center mb-4">
+        <div>
+          <h2 className="mb-1">Create Alert Definition</h2>
+          <div className="text-muted">Step {step} of 3</div>
+        </div>
+        <Button variant="outline-secondary" onClick={() => navigate("/main/alerts")}>Cancel</Button>
+      </div>
+
+      {errors.length > 0 && (
+        <Alert variant="danger">
+          {errors.map((error) => <div key={error}>{error}</div>)}
+        </Alert>
+      )}
+
+      {step === 1 && (
+        <Row className="g-3" aria-label="Alert type">
+          {TYPE_OPTIONS.map((option) => (
+            <Col md={6} key={option.type}>
+              <Button
+                variant="outline-secondary"
+                className="w-100 d-flex align-items-center justify-content-start gap-3 p-4 text-start"
+                onClick={() => chooseType(option.type)}
+              >
+                <FontAwesomeIcon icon={option.icon} size="2x" />
+                <strong>{option.label}</strong>
+              </Button>
+            </Col>
+          ))}
+        </Row>
+      )}
+
+      {step === 2 && form.type && (
+        <Form>
+          {commonFields}
+          <hr className="my-4" />
+          <Row className="g-3">
+            {(form.type === "PORT" || form.type === "WEB") && (
+              <>
+                <Col md={8}>
+                  <Form.Group controlId="alert-uri">
+                    <Form.Label>{form.type === "PORT" ? "Host or URI" : "HTTP URI"}</Form.Label>
+                    <Form.Control value={form.uri} onChange={(event) => update("uri", event.target.value)} />
+                  </Form.Group>
+                </Col>
+                <Col md={4}>
+                  <Form.Group controlId="alert-port">
+                    <Form.Label>Default Port</Form.Label>
+                    <Form.Control type="number" min={1} max={65535} value={form.defaultPort} onChange={(event) => update("defaultPort", event.target.value)} />
+                  </Form.Group>
+                </Col>
+              </>
+            )}
+            {form.type === "WEB" && (
+              <Col md={4}>
+                <Form.Group controlId="alert-timeout">
+                  <Form.Label>Connection Timeout</Form.Label>
+                  <Form.Control type="number" min={0.1} step={0.1} value={form.connectionTimeout} onChange={(event) => update("connectionTimeout", event.target.value)} />
+                </Form.Group>
+              </Col>
+            )}
+            {form.type === "SCRIPT" && (
+              <Col xs={12}>
+                <Form.Group controlId="alert-script-path">
+                  <Form.Label>Script Path</Form.Label>
+                  <Form.Control value={form.scriptPath} onChange={(event) => update("scriptPath", event.target.value)} />
+                </Form.Group>
+              </Col>
+            )}
+            {form.type === "AGGREGATE" && (
+              <Col xs={12}>
+                <Form.Group controlId="alert-reference">
+                  <Form.Label>Referenced Alert Name</Form.Label>
+                  <Form.Control list="alert-definition-names" value={form.aggregateAlertName} onChange={(event) => update("aggregateAlertName", event.target.value)} />
+                  <datalist id="alert-definition-names">
+                    {aggregateDefinitions.map((name) => <option key={name} value={name} />)}
+                  </datalist>
+                </Form.Group>
+              </Col>
+            )}
+            {(form.type === "PORT" || form.type === "AGGREGATE") && (
+              <>
+                <Col md={6}>
+                  <Form.Group controlId="alert-warning-threshold">
+                    <Form.Label>Warning Threshold</Form.Label>
+                    <Form.Control type="number" min={0} step="any" value={form.warningThreshold} onChange={(event) => update("warningThreshold", event.target.value)} />
+                  </Form.Group>
+                </Col>
+                <Col md={6}>
+                  <Form.Group controlId="alert-critical-threshold">
+                    <Form.Label>Critical Threshold</Form.Label>
+                    <Form.Control type="number" min={0} step="any" value={form.criticalThreshold} onChange={(event) => update("criticalThreshold", event.target.value)} />
+                  </Form.Group>
+                </Col>
+              </>
+            )}
+            {form.type !== "SCRIPT" && (
+              <>
+                <Col md={4}><Form.Group controlId="alert-ok-text"><Form.Label>OK Text</Form.Label><Form.Control value={form.okText} onChange={(event) => update("okText", event.target.value)} /></Form.Group></Col>
+                <Col md={4}><Form.Group controlId="alert-warning-text"><Form.Label>Warning Text</Form.Label><Form.Control value={form.warningText} onChange={(event) => update("warningText", event.target.value)} /></Form.Group></Col>
+                <Col md={4}><Form.Group controlId="alert-critical-text"><Form.Label>Critical Text</Form.Label><Form.Control value={form.criticalText} onChange={(event) => update("criticalText", event.target.value)} /></Form.Group></Col>
+              </>
+            )}
+          </Row>
+          <div className="d-flex justify-content-between mt-4">
+            <Button variant="outline-secondary" onClick={() => navigate("/main/alerts/add/1")}>Back</Button>
+            <Button variant="primary" onClick={continueToReview}>Next</Button>
+          </div>
+        </Form>
+      )}
+
+      {step === 3 && form.type && (
+        <>
+          <h3 className="h5 mb-3">Review</h3>
+          <pre className="border bg-light p-3 overflow-auto">{JSON.stringify(buildAlertCreationPayload(form), null, 2)}</pre>
+          <div className="d-flex justify-content-between mt-4">
+            <Button variant="outline-secondary" disabled={isSubmitting} onClick={() => navigate("/main/alerts/add/2")}>Back</Button>
+            <Button variant="primary" disabled={isSubmitting} onClick={submit}>
+              {isSubmitting ? <><Spinner size="sm" className="me-2" />Creating</> : "Create"}
+            </Button>
+          </div>
+        </>
+      )}
+    </Container>
+  );
+};
+
+export default AlertDefinitionWizard;
diff --git a/ambari-web/latest/src/screens/Alerts/AlertInstancesTable.test.tsx b/ambari-web/latest/src/screens/Alerts/AlertInstancesTable.test.tsx
new file mode 100644
index 0000000000..b6dae4f197
--- /dev/null
+++ b/ambari-web/latest/src/screens/Alerts/AlertInstancesTable.test.tsx
@@ -0,0 +1,101 @@
+/**
+ * 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 { act, cleanup, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { MemoryRouter } from "react-router-dom";
+
+const mocks = vi.hoisted(() => ({
+  getAlertInstancesByDefinition: vi.fn(),
+  getAlertHistory: vi.fn(),
+}));
+
+vi.mock("../../api/alertsApi", () => ({ AlertsApi: mocks }));
+vi.mock("../../components/Spinner", () => ({ default: () => <div>Loading instances</div> }));
+vi.mock("../../components/Paginator", () => ({ default: () => <div>Pagination</div> }));
+vi.mock("../../components/Table", () => ({
+  default: ({ data }: { data: Array<{ host_name: string }> }) => (
+    <div>{data.map((item) => <div key={item.host_name}>{item.host_name}</div>)}</div>
+  ),
+}));
+
+import AlertInstancesTable from "./AlertInstancesTable";
+
+const instancesResponse = {
+  items: [{
+    Alert: {
+      service_name: "HDFS",
+      host_name: "host1.example.test",
+      state: "OK",
+      maintenance_state: "OFF",
+      last_updated_time: 1,
+      text: "Healthy",
+    },
+  }],
+};
+
+function renderTable() {
+  return render(
+    <MemoryRouter>
+      <AlertInstancesTable
+        clusterName="c1"
+        alert_id={11}
+        definitionName="namenode_process"
+      />
+    </MemoryRouter>,
+  );
+}
+
+describe("AlertInstancesTable loading and polling", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.spyOn(console, "error").mockImplementation(() => undefined);
+    mocks.getAlertInstancesByDefinition.mockResolvedValue(instancesResponse);
+    mocks.getAlertHistory.mockResolvedValue({ items: [] });
+  });
+
+  afterEach(() => {
+    cleanup();
+    vi.useRealTimers();
+    vi.restoreAllMocks();
+  });
+
+  it("keeps available instances when the independent history request fails", async () => {
+    mocks.getAlertHistory.mockRejectedValueOnce(new Error("history unavailable"));
+    renderTable();
+
+    expect(await screen.findByText("host1.example.test")).toBeTruthy();
+    expect(screen.getByText("24-hour alert history is temporarily unavailable.")).toBeTruthy();
+    expect(screen.queryByText("Failed to load alert instances.")).toBeNull();
+  });
+
+  it("recovers an initial instance failure after a successful poll", async () => {
+    vi.useFakeTimers();
+    mocks.getAlertInstancesByDefinition
+      .mockRejectedValueOnce(new Error("instances unavailable"))
+      .mockResolvedValueOnce(instancesResponse);
+    renderTable();
+
+    await act(async () => undefined);
+    expect(screen.getByText("Failed to load alert instances.")).toBeTruthy();
+
+    await act(async () => vi.advanceTimersByTimeAsync(30000));
+    expect(screen.getByText("host1.example.test")).toBeTruthy();
+    expect(screen.queryByText("Failed to load alert instances.")).toBeNull();
+  });
+});
diff --git a/ambari-web/latest/src/screens/Alerts/AlertInstancesTable.tsx b/ambari-web/latest/src/screens/Alerts/AlertInstancesTable.tsx
index e1a01ba417..6d533f9b52 100644
--- a/ambari-web/latest/src/screens/Alerts/AlertInstancesTable.tsx
+++ b/ambari-web/latest/src/screens/Alerts/AlertInstancesTable.tsx
@@ -24,7 +24,6 @@ import "../../styles/app.scss";
 import { Button, Form } from 'react-bootstrap';
 import Modal from '../../components/Modal';
 import CopyButton from '../../components/CopyButton';
-import { isWithin24Hours } from '../../Utils/Utility';
 import Paginator from "../../components/Paginator";
 import usePagination from '../../hooks/usePagination';
 import { SortingState } from "@tanstack/react-table";
@@ -33,10 +32,16 @@ import LastStatusChanged from "../../components/LastStatusChanged";
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faBriefcase } from '@fortawesome/free-solid-svg-icons';
 import { formatStatus } from '../../Utils/Utility';
+import {
+    countAlertHistoryByHost,
+    filterAndSortAlertInstances,
+    openAlertResponseInNewWindow,
+} from '../../Utils/alertDefinitions';
 
 interface AlertInstancesTableProps {
     clusterName: string;
     alert_id: number | string;
+    definitionName: string;
     refreshTrigger?: number;
     alertEnabled?: boolean;
 }
@@ -57,10 +62,9 @@ interface FilterState {
     state: string;
 }
 
-const AlertInstancesTable = ({ clusterName, alert_id, refreshTrigger, alertEnabled = true }: AlertInstancesTableProps) => {
+const AlertInstancesTable = ({ clusterName, alert_id, definitionName, refreshTrigger, alertEnabled = true }: AlertInstancesTableProps) => {
     const [isLoaded, setIsLoaded] = useState(false);
     const [instances, setInstances] = useState<AlertInstance[]>([]);
-    const [filteredInstances, setFilteredInstances] = useState<AlertInstance[]>([]);
     const [filters, setFilters] = useState<FilterState>({
         service: '',
         hostName: '',
@@ -70,130 +74,98 @@ const AlertInstancesTable = ({ clusterName, alert_id, refreshTrigger, alertEnabl
     const [modalText, setModalText] = useState('');
     const [sorting, setSorting] = useState<SortingState>([]);
     const [availableServices, setAvailableServices] = useState<string[]>([]);
-    //@ts-ignore
-    const copyToClipboard = async (text: string) => {
-        try {
-            // Check if clipboard API is available (HTTPS required)
-            if (navigator.clipboard && window.isSecureContext) {
-                await navigator.clipboard.writeText(text);
-                return;
-            }
-            
-            // Fallback for HTTP or older browsers
-            const textArea = document.createElement('textarea');
-            textArea.value = text;
-            textArea.style.position = 'fixed';
-            textArea.style.left = '-999999px';
-            textArea.style.top = '-999999px';
-            document.body.appendChild(textArea);
-            textArea.focus();
-            textArea.select();
-            
-            try {
-                document.execCommand('copy');
-            } catch (fallbackErr) {
-                console.error("Fallback copy failed: ", fallbackErr);
-                // Show user a message to manually copy
-                alert(`Please copy this manually: ${text}`);
-            }
-            
-            document.body.removeChild(textArea);
-        } catch (err) {
-            console.error('Failed to copy:', err);
-            // Final fallback - show text for manual copy
-            alert(`Please copy this manually: ${text}`);
-        }
-    };
+    const [historyCounts, setHistoryCounts] = useState<Record<string, number>>({});
+    const [loadError, setLoadError] = useState('');
+    const [historyError, setHistoryError] = useState('');
+    const [refreshError, setRefreshError] = useState('');
+    const [retryTrigger, setRetryTrigger] = useState(0);
 
     useEffect(() => {
+        let active = true;
+        let pollTimer: ReturnType<typeof setTimeout> | undefined;
+
+        const applyInstances = (data: any) => {
+            const instancesData = (data?.items || []).map(
+                (alertObj: { Alert: AlertInstance }) => alertObj.Alert,
+            );
+            const services = [...new Set(instancesData.map(
+                (instance: AlertInstance) => instance.service_name?.toString() || '',
+            ))] as string[];
+            setAvailableServices(services);
+            setInstances(instancesData);
+        };
+
         const fetchData = async () => {
             // If alert is disabled, don't fetch data and clear existing instances
             if (!alertEnabled) {
                 setInstances([]);
-                setFilteredInstances([]);
                 setAvailableServices([]);
+                setHistoryCounts({});
+                setHistoryError('');
                 setIsLoaded(true);
                 return;
             }
             
             setIsLoaded(false);
-            
-            try {
-                const time = new Date().getTime();
-                // FIXED: Get all alert instances including maintenance mode ones
-                const data = await AlertsApi.getAlertsList(clusterName, time);
-                // Filter by the specific alert definition ID
-                const allInstances = data.items.map((alertObj: { Alert: AlertInstance }) => alertObj.Alert);
-                const instancesData = allInstances.filter((instance: AlertInstance) => 
-                    instance.definition_id === parseInt(String(alert_id))
-                );
+            setLoadError('');
+            setHistoryError('');
+            setRefreshError('');
 
-                // Extract unique service names for the service filter
-                const services = [...new Set(instancesData.map((instance: { service_name: { toString: () => any; }; }) => instance.service_name?.toString() || ''))] as string[];
-                setAvailableServices(services);
+            const now = Date.now();
+            const [instancesResult, historyResult] = await Promise.allSettled([
+                AlertsApi.getAlertInstancesByDefinition(clusterName, alert_id, now),
+                AlertsApi.getAlertHistory(clusterName, definitionName, now - 24 * 60 * 60 * 1000),
+            ]);
+            if (!active) return;
 
-                setInstances(instancesData);
-                setFilteredInstances(instancesData);
-                setIsLoaded(true);
-            } catch (error) {
-                console.error("Error fetching alert instances:", error);
-                setIsLoaded(true);
+            if (instancesResult.status === 'fulfilled') {
+                applyInstances(instancesResult.value);
+            } else {
+                console.error("Error fetching alert instances:", instancesResult.reason);
                 setInstances([]);
-                setFilteredInstances([]);
+                setLoadError('Failed to load alert instances.');
             }
-        };
-
-        fetchData();
-    }, [clusterName, alert_id, refreshTrigger, alertEnabled]);
 
-    useEffect(() => {
-        let filtered = instances;
-        if (filters.service && filters.service !== 'All') {
-            filtered = filtered.filter(
-                (instance) => instance.service_name === filters.service
-            );
-        }
-        if (filters.hostName) {
-            filtered = filtered.filter(
-                (instance) => instance.host_name.toLowerCase().includes(filters.hostName.toLowerCase())
-            );
-        }
-        if (filters.state && filters.state !== 'All') {
-            filtered = filtered.filter((instance) => instance.state.toLowerCase() === filters.state.toLowerCase());
-        }
-        setFilteredInstances(filtered);
-    }, [instances, filters]);
-
-    // Apply sorting
-    useEffect(() => {
-        if (sorting.length === 0) return;
-
-        const sortedData = [...filteredInstances].sort((a, b) => {
-            const sortField = sorting[0].id;
-            const isAsc = sorting[0].desc === false;
-            
-            if (sortField === 'last_updated_time') {
-                const aValue = a.last_updated_time || 0;
-                const bValue = b.last_updated_time || 0;
-                return isAsc ? aValue - bValue : bValue - aValue;
+            if (historyResult.status === 'fulfilled') {
+                setHistoryCounts(countAlertHistoryByHost(historyResult.value?.items || []));
+            } else {
+                console.error("Error fetching 24-hour alert history:", historyResult.reason);
+                setHistoryCounts({});
+                setHistoryError('24-hour alert history is temporarily unavailable.');
             }
-            
-            const aValue = a[sortField] || '';
-            const bValue = b[sortField] || '';
-            
-            if (typeof aValue === 'string' && typeof bValue === 'string') {
-                return isAsc 
-                    ? aValue.localeCompare(bValue) 
-                    : bValue.localeCompare(aValue);
+            setIsLoaded(true);
+        };
+
+        const pollInstances = async () => {
+            try {
+                const data = await AlertsApi.getAlertInstancesByDefinition(clusterName, alert_id, Date.now());
+                if (!active) return;
+                applyInstances(data);
+                setLoadError('');
+                setRefreshError('');
+                setIsLoaded(true);
+            } catch (error) {
+                if (active) setRefreshError('The latest instance refresh failed. Existing data is still displayed.');
+            } finally {
+                if (active) pollTimer = setTimeout(pollInstances, 30000);
             }
-            
-            return isAsc 
-                ? (aValue > bValue ? 1 : -1) 
-                : (bValue > aValue ? 1 : -1);
+        };
+
+        void fetchData().then(() => {
+            if (active && alertEnabled) pollTimer = setTimeout(pollInstances, 30000);
         });
-        
-        setFilteredInstances(sortedData);
-    }, [sorting]);
+        return () => {
+            active = false;
+            if (pollTimer) clearTimeout(pollTimer);
+        };
+    }, [clusterName, alert_id, definitionName, refreshTrigger, alertEnabled, retryTrigger]);
+
+    const filteredInstances = filterAndSortAlertInstances(
+        instances,
+        filters,
+        sorting[0],
+        historyCounts,
+    );
 
     // Use pagination hook
     const {
@@ -296,10 +268,11 @@ const AlertInstancesTable = ({ clusterName, alert_id, refreshTrigger, alertEnabl
         },
         {
             header: '24-Hour',
-            accessorKey: 'last_updated_time',
+            accessorKey: 'history_count',
+            sortingFn: (rowA: { original: AlertInstance }, rowB: { original: AlertInstance }) =>
+                (historyCounts[rowA.original.host_name] || 0) - (historyCounts[rowB.original.host_name] || 0),
             cell: ({ row }: { row: { original: AlertInstance } }) => {
-                const lastUpdatedTime = row.original.last_updated_time;
-                return isWithin24Hours(lastUpdatedTime);
+                return historyCounts[row.original.host_name] || 0;
             },
         },
         {
@@ -332,10 +305,33 @@ const AlertInstancesTable = ({ clusterName, alert_id, refreshTrigger, alertEnabl
         return <Spinner />;
     }
 
+    if (loadError) {
+        return (
+            <div className="col-md-12">
+                <div className="alert alert-danger">{loadError}</div>
+                <Button variant="primary" onClick={() => setRetryTrigger((value) => value + 1)}>Retry</Button>
+            </div>
+        );
+    }
+
     return (
         <div className="col-md-12">
             <h2 className="table-title mb-3">Instances</h2>
 
+            {refreshError && (
+                <div className="alert alert-warning d-flex justify-content-between align-items-center">
+                    <span>{refreshError}</span>
+                    <Button variant="outline-warning" onClick={() => setRetryTrigger((value) => value + 1)}>Retry</Button>
+                </div>
+            )}
+
+            {historyError && (
+                <div className="alert alert-warning d-flex justify-content-between align-items-center">
+                    <span>{historyError}</span>
+                    <Button variant="outline-warning" onClick={() => setRetryTrigger((value) => value + 1)}>Retry</Button>
+                </div>
+            )}
+
             {/* Filters - only show when alert is enabled */}
             {alertEnabled && (
                 <div className="mb-3 d-flex">
@@ -430,10 +426,7 @@ const AlertInstancesTable = ({ clusterName, alert_id, refreshTrigger, alertEnabl
                         <div className="controls-block pull-right">
                             <CopyButton textToCopy={modalText} buttonText="Copy" />
                             <a title="Open in New Window" onClick={() => {
-                                const newWindow = window.open();
-                                if (newWindow) {
-                                    newWindow.document.write(`<pre>${modalText}</pre>`);
-                                }
+                                openAlertResponseInNewWindow(modalText);
                             }} className="task-detail-open-dialog">
                                 <i className="icon-external-link"></i> <span>Open</span>
                             </a>
diff --git a/ambari-web/latest/src/screens/Alerts/Alerts.tsx b/ambari-web/latest/src/screens/Alerts/Alerts.tsx
index 709479b15c..5fcb23dbb3 100644
--- a/ambari-web/latest/src/screens/Alerts/Alerts.tsx
+++ b/ambari-web/latest/src/screens/Alerts/Alerts.tsx
@@ -24,7 +24,7 @@ import Spinner from "../../components/Spinner";
 import {useEffect, useState, useContext, useCallback} from 'react';
 import {AlertsApi} from "../../api/alertsApi";
 import {getCurrTimeInSec} from "../../Utils/Utility";
-import {AlertDefinition, AlertGroupItem, AlertRow, MergedAlert} from "./types";
+import {AlertDefinition, AlertGroupItem, AlertRow, MergedAlert, SearchFilter} from "./types";
 import {Container} from 'react-bootstrap';
 import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
 import {faPowerOff, faBriefcase} from '@fortawesome/free-solid-svg-icons';
@@ -39,43 +39,48 @@ import {formatAlertStatusDisplay} from "./alertStatus";
 import { useAuth } from '../../hooks/useAuth';
 import usePolling from '../../hooks/usePolling';
 
-interface SearchFilter {
-    category: string;
-    value: string;
-}
+const DEFAULT_ALERT_SORTING: SortingState = [{ id: 'statuses', desc: true }];
+
+const loadAlertListViewState = (clusterName: string): {
+    sorting: SortingState;
+    searchFilters: SearchFilter[];
+} => {
+    if (!clusterName || typeof window === 'undefined') {
+        return { sorting: DEFAULT_ALERT_SORTING, searchFilters: [] };
+    }
+    try {
+        const saved = JSON.parse(window.sessionStorage.getItem(`ambari.alerts.view.${clusterName}`) || '{}');
+        return {
+            sorting: Array.isArray(saved.sorting) ? saved.sorting : DEFAULT_ALERT_SORTING,
+            searchFilters: Array.isArray(saved.searchFilters) ? saved.searchFilters : [],
+        };
+    } catch {
+        return { sorting: DEFAULT_ALERT_SORTING, searchFilters: [] };
+    }
+};
 
 const Alerts = () => {
-    const { clusterName, upgradeIsRunning, upgradeSuspended } = useContext(AppContext);
+    const { clusterName } = useContext(AppContext);
+    const initialViewState = loadAlertListViewState(clusterName);
     const [alerts, setAlerts] = useState<MergedAlert[]>([]);
     const [isLoading, setIsLoading] = useState(true);
-    const [sorting, setSorting] = useState<SortingState>([
-        {
-            id: 'statuses',
-            desc: true // Sort in descending order to show critical alerts first
-        }
-    ]);
+    const [sorting, setSorting] = useState<SortingState>(initialViewState.sorting);
     const [alertGroups, setAlertGroups] = useState<AlertGroupItem[]>([]);
     const [alertDefinitions, setAlertDefinitions] = useState<AlertDefinition[]>([]);
-    const [searchFilters, setSearchFilters] = useState<SearchFilter[]>([]);
+    const [searchFilters, setSearchFilters] = useState<SearchFilter[]>(initialViewState.searchFilters);
     const [filteredAlerts, setFilteredAlerts] = useState<MergedAlert[]>([]);
     const [isModalOpen, setIsModalOpen] = useState(false);
+    const [listLoadError, setListLoadError] = useState('');
+    const [definitionLoadError, setDefinitionLoadError] = useState('');
     
     // Authorization hooks - implementing Ember.js alert authorization patterns
     const { hasAuthorization } = useAuth();
     
     // Check specific authorizations for alert operations
-    const canToggleAlerts = hasAuthorization('SERVICE.TOGGLE_ALERTS');
-    const canManageNotifications = hasAuthorization('CLUSTER.MANAGE_ALERT_NOTIFICATIONS');
-    
-    // Check if user has any alert management permissions
-    const hasAnyAlertPermissions = canToggleAlerts || canManageNotifications;
-    
-    // Check if upgrade is blocking operations (running but not suspended)
-    const isUpgradeBlocking = upgradeIsRunning && !upgradeSuspended;
+    const canToggleAlerts = hasAuthorization('CLUSTER.TOGGLE_ALERTS');
 
     const countAlertsByService = (alertGroups: AlertGroupItem[]) => {
         if (!alertGroups || !Array.isArray(alertGroups) || alertGroups.length === 0) {
-            console.log('No alert groups available for counting');
             return {};
         }
 
@@ -102,13 +107,12 @@ const Alerts = () => {
 
     const fetchData = useCallback(async () => {
         if (!clusterName) {
-            console.error('No cluster name provided');
             return;
         }
 
         const currTime = getCurrTimeInSec();
         try {
-            console.log('Fetching alerts for cluster:', clusterName);
+            setListLoadError('');
             const [alertsResponse, summariesResponse] = await Promise.all([
                 AlertsApi.getAlerts(
                     clusterName,
@@ -118,21 +122,16 @@ const Alerts = () => {
                 AlertsApi.getGroupFormattedAlertsNotifications(clusterName, currTime),
             ]);
 
-            console.log('Alert Groups Response:', alertsResponse);
-            console.log('Alert Summary Response:', summariesResponse);
-
             if (!alertsResponse || !alertsResponse.items) {
-                console.error('Invalid alert groups response:', alertsResponse);
-                return;
+                throw new Error('Invalid Alert Groups response');
             }
 
             const processedAlerts = processData(alertsResponse, summariesResponse);
             setAlerts(processedAlerts);
             const alertGroups = alertsResponse.items;
-            console.log('Setting alert groups:', alertGroups);
             setAlertGroups(alertGroups);
-        } catch (error) {
-            console.error('Error fetching data:', error);
+        } catch {
+            setListLoadError('Ambari could not load Alerts. Retry the request.');
         } finally {
             setIsLoading(false);
         }
@@ -142,14 +141,19 @@ const Alerts = () => {
     const { pausePolling, resumePolling } = usePolling(fetchData, 30000);
 
     useEffect(() => {
-        console.log('Initial render with clusterName:', clusterName);
         if (clusterName) {
             fetchAlertDefinitions();
-        } else {
-            console.error('No cluster name available');
         }
     }, [clusterName]);
 
+    useEffect(() => {
+        if (!clusterName) return;
+        window.sessionStorage.setItem(`ambari.alerts.view.${clusterName}`, JSON.stringify({
+            sorting,
+            searchFilters,
+        }));
+    }, [clusterName, searchFilters, sorting]);
+
     // Control polling based on modal state
     useEffect(() => {
         if (isModalOpen) {
@@ -173,13 +177,13 @@ const Alerts = () => {
                     label: item.AlertDefinition.label || item.AlertDefinition.name,
                     component_name: item.AlertDefinition.component_name || 'N/A'
                 }));
-                console.log('Fetched alert definitions:', definitions);
                 setAlertDefinitions(definitions);
+                setDefinitionLoadError('');
             } else {
-                console.error('No alert definitions found in response:', response);
+                throw new Error('No Alert Definitions in response');
             }
-        } catch (error) {
-            console.error('Error fetching alert definitions:', error);
+        } catch {
+            setDefinitionLoadError('Ambari could not load Alert Definitions. Retry the request.');
         }
     };
 
@@ -232,15 +236,16 @@ const Alerts = () => {
         const [isEnabled, setIsEnabled] = useState(alert.enabled);
         const [showModal, setShowModal] = useState(false);
         const [isUpdating, setIsUpdating] = useState(false);
+        const [toggleError, setToggleError] = useState('');
 
         const handleToggleState = () => {
+            setToggleError('');
             setShowModal(true);
         };
 
         const handleConfirmToggle = async () => {
             if (!alert.id) {
-                console.error('Alert ID is missing');
-                setShowModal(false);
+                setToggleError('Alert Definition ID is missing.');
                 return;
             }
             
@@ -252,11 +257,11 @@ const Alerts = () => {
                 // Refresh the alerts list
                 fetchData();
                 fetchAlertDefinitions();
-            } catch (error) {
-                console.error('Error updating alert state:', error);
+                setShowModal(false);
+            } catch {
+                setToggleError('Ambari could not update the Alert Definition. Retry the request.');
             } finally {
                 setIsUpdating(false);
-                setShowModal(false);
             }
         };
 
@@ -266,8 +271,7 @@ const Alerts = () => {
 
         return (
             <>
-                {/* Show enabled status for all users, but only make it clickable for authorized users and when not upgrading */}
-                {canToggleAlerts && !isUpgradeBlocking ? (
+                {canToggleAlerts ? (
                     <div className="custom-link" onClick={handleToggleState}>
                         <FontAwesomeIcon className={'mx-1'} icon={faPowerOff} />
                         {isEnabled ? 'Enabled' : 'Disabled'}
@@ -283,7 +287,10 @@ const Alerts = () => {
                     isOpen={showModal}
                     onClose={handleCancelToggle}
                     modalTitle="Confirmation"
-                    modalBody={isEnabled ? "You are about to Disable this alert definition" : "You are about to Enable this alert definition"}
+                    modalBody={<>
+                        <div>{isEnabled ? "You are about to Disable this alert definition" : "You are about to Enable this alert definition"}</div>
+                        {toggleError && <div className="alert alert-danger mt-3">{toggleError}</div>}
+                    </>}
                     successCallback={handleConfirmToggle}
                     options={{
                         okButtonText: isEnabled ? "Confirm Disable" : "Confirm Enable",
@@ -554,14 +561,23 @@ const Alerts = () => {
                 <Spinner />
             ) : (
                 <>
+                    {(listLoadError || definitionLoadError) && (
+                        <div className="alert alert-danger d-flex justify-content-between align-items-center">
+                            <span>{listLoadError || definitionLoadError}</span>
+                            <button className="btn btn-outline-danger" onClick={() => {
+                                setIsLoading(true);
+                                void Promise.all([fetchData(), fetchAlertDefinitions()]);
+                            }}>Retry</button>
+                        </div>
+                    )}
                     <MenuBar
                         title="Alerts"
                         alertGroups={alertGroups}
                         alertCounts={alertCounts}
                         onSearch={handleSearch}
+                        searchFilters={searchFilters}
                         alertDefinitions={alertDefinitions}
                         onModalStateChange={setIsModalOpen}
-                        hasAnyAlertPermissions={hasAnyAlertPermissions}
                     />
                     <Table
                         columns={columns}
diff --git a/ambari-web/latest/src/screens/Alerts/HeaderSection.tsx b/ambari-web/latest/src/screens/Alerts/HeaderSection.tsx
index b43c304084..0d625eb9a3 100644
--- a/ambari-web/latest/src/screens/Alerts/HeaderSection.tsx
+++ b/ambari-web/latest/src/screens/Alerts/HeaderSection.tsx
@@ -16,14 +16,14 @@
  * limitations under the License.
  */
 
-import { useState, useContext } from 'react';
+import { forwardRef, useContext, useEffect, useImperativeHandle, useState } from 'react';
 import { Link } from "react-router-dom";
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faPencil, faArrowLeft, faBriefcase } from "@fortawesome/free-solid-svg-icons";
 import { Button, Form, Row, Col } from 'react-bootstrap';
 import { getStatusClass, formatStatus } from '../../Utils/Utility';
 import "../../styles/app.scss";
-import { MergedAlert } from './types';
+import { AlertEditorHandle, MergedAlert } from './types';
 import { AlertsApi } from "../../api/alertsApi";
 import { AppContext } from "../../store/context";
 import { useAuth } from '../../hooks/useAuth';
@@ -41,14 +41,22 @@ interface StatusItem {
 interface HeaderSectionProps {
     alertDefinition: MergedAlert;
     statuses: StatusItem[];
+    onDirtyChange?: (dirty: boolean) => void;
+    onAlertDefinitionUpdate?: (updatedDefinition: Partial<MergedAlert>) => void;
 }
 
-const HeaderSection = ({ alertDefinition, statuses }: HeaderSectionProps) => {
+const HeaderSection = forwardRef<AlertEditorHandle, HeaderSectionProps>(({
+    alertDefinition,
+    statuses,
+    onDirtyChange,
+    onAlertDefinitionUpdate,
+}, ref) => {
     const { clusterName, upgradeIsRunning, upgradeSuspended } = useContext(AppContext);
     const [isEditing, setIsEditing] = useState(false);
     const [label, setLabel] = useState(alertDefinition.label);
     const [hasError, setHasError] = useState(false);
     const [errorMessage, setErrorMessage] = useState('');
+    const [saveError, setSaveError] = useState('');
     const [isSubmitting, setIsSubmitting] = useState(false);
     
     // Authorization hooks - implementing Ember.js alert authorization patterns
@@ -60,6 +68,10 @@ const HeaderSection = ({ alertDefinition, statuses }: HeaderSectionProps) => {
     // Check if upgrade is blocking operations (running but not suspended)
     const isUpgradeBlocking = upgradeIsRunning && !upgradeSuspended;
 
+    useEffect(() => {
+        if (!isEditing) setLabel(alertDefinition.label);
+    }, [alertDefinition.label, isEditing]);
+
     // Validation regex for alert definition name
     const validNameRegex = /^[a-zA-Z0-9_\-\s\[\]%]+$/;
     const maxLength = 255;
@@ -86,6 +98,8 @@ const HeaderSection = ({ alertDefinition, statuses }: HeaderSectionProps) => {
 
     const handleEditClick = () => {
         setIsEditing(true);
+        setSaveError('');
+        onDirtyChange?.(true);
     };
 
     const handleCancelEdit = () => {
@@ -93,18 +107,21 @@ const HeaderSection = ({ alertDefinition, statuses }: HeaderSectionProps) => {
         setLabel(alertDefinition.label);
         setHasError(false);
         setErrorMessage('');
+        setSaveError('');
+        onDirtyChange?.(false);
     };
 
     const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
         const value = e.target.value;
         setLabel(value);
         setHasError(!validateInput(value));
+        setSaveError('');
     };
 
-    const handleSaveEdit = async () => {
+    const handleSaveEdit = async (): Promise<boolean> => {
         if (!validateInput(label)) {
             setHasError(true);
-            return;
+            return false;
         }
 
         setIsSubmitting(true);
@@ -118,18 +135,25 @@ const HeaderSection = ({ alertDefinition, statuses }: HeaderSectionProps) => {
                 }
             );
             
-            // Update local state
-            alertDefinition.label = label;
+            onAlertDefinitionUpdate?.({ label });
             setIsEditing(false);
+            setSaveError('');
+            onDirtyChange?.(false);
+            return true;
         } catch (error) {
             console.error('Failed to update alert definition:', error);
-            setErrorMessage('Failed to update alert definition. Please try again.');
-            setHasError(true);
+            setSaveError('Failed to update alert definition. Please try again.');
+            return false;
         } finally {
             setIsSubmitting(false);
         }
     };
 
+    useImperativeHandle(ref, () => ({
+        save: handleSaveEdit,
+        discard: handleCancelEdit,
+    }));
+
     return (
         <Row className="alert-definition-details-header gap-4 mb-4">
             <Col md={8} className="my-2">
@@ -149,6 +173,7 @@ const HeaderSection = ({ alertDefinition, statuses }: HeaderSectionProps) => {
                                         {errorMessage}
                                     </div>
                                 )}
+                                {saveError && <div className="text-danger mt-1">{saveError}</div>}
                             </Col>
                             <Col md={4} className="edit-buttons align-content-center align-items-end">
                                 <Button 
@@ -234,6 +259,8 @@ const HeaderSection = ({ alertDefinition, statuses }: HeaderSectionProps) => {
             </Col>
         </Row>
     );
-};
+});
+
+HeaderSection.displayName = 'HeaderSection';
 
 export default HeaderSection;
diff --git a/ambari-web/latest/src/screens/Alerts/InformationSection.tsx b/ambari-web/latest/src/screens/Alerts/InformationSection.tsx
index a90ed7d5ef..0b72847527 100644
--- a/ambari-web/latest/src/screens/Alerts/InformationSection.tsx
+++ b/ambari-web/latest/src/screens/Alerts/InformationSection.tsx
@@ -35,16 +35,7 @@ import { ambariApi } from "../../api/config/axiosConfig";
 import { AppContext } from "../../store/context";
 import { useParams } from "react-router-dom";
 import { useAuth } from "../../hooks/useAuth";
-
-interface AlertDefinitionResponseItem {
-  AlertDefinition: {
-    id: number;
-    repeat_tolerance: any;
-    repeat_tolerance_enabled: boolean;
-    help_url?: string;
-    [key: string]: any;
-  };
-}
+import { validateRepeatTolerance } from '../../Utils/alertDefinitions';
 
 const InformationSection = ({
   alertDefinition,
@@ -57,13 +48,13 @@ const InformationSection = ({
     useContext(AppContext);
   const params = useParams<{ alertId?: string }>();
   const [showToleranceModal, setShowToleranceModal] = useState(false);
-  const [toleranceValue, setToleranceValue] = useState<number>(
-    alertDefinition.repeat_tolerance || 1
+  const [toleranceValue, setToleranceValue] = useState<string>(
+    String(alertDefinition.repeat_tolerance ?? 1)
   );
   const [isUpdatingTolerance, setIsUpdatingTolerance] = useState(false);
   const [toleranceError, setToleranceError] = useState<string | null>(null);
-  const [localRepeatTolerance, setLocalRepeatTolerance] = useState<number>(
-    alertDefinition.repeat_tolerance || 1
+  const [localRepeatTolerance, setLocalRepeatTolerance] = useState<string>(
+    String(alertDefinition.repeat_tolerance ?? 1)
   );
   const [localRepeatToleranceEnabled, setLocalRepeatToleranceEnabled] =
     useState<boolean>(alertDefinition.repeat_tolerance_enabled || false);
@@ -91,13 +82,7 @@ const InformationSection = ({
   });
 
   useEffect(() => {
-    console.log("InformationSection - Alert Definition changed:", {
-      repeat_tolerance: alertDefinition.repeat_tolerance,
-      repeat_tolerance_enabled: alertDefinition.repeat_tolerance_enabled,
-      alert_definition_id: alertDefinition.alert_definition_id,
-    });
-
-    setLocalRepeatTolerance(alertDefinition.repeat_tolerance || 1);
+    setLocalRepeatTolerance(String(alertDefinition.repeat_tolerance ?? 1));
     setLocalRepeatToleranceEnabled(
       alertDefinition.repeat_tolerance_enabled || false
     );
@@ -177,31 +162,21 @@ const InformationSection = ({
     try {
       const alertDefinitionId = params.alertId;
       if (alertDefinitionId) {
-        const alertDefinitionResponse = await AlertsApi.getAlertDefinition(
-          clusterName,
-          "AlertDefinition/component_name,AlertDefinition/description,AlertDefinition/enabled,AlertDefinition/repeat_tolerance,AlertDefinition/repeat_tolerance_enabled,AlertDefinition/id,AlertDefinition/ignore_host,AlertDefinition/interval,AlertDefinition/label,AlertDefinition/name,AlertDefinition/scope,AlertDefinition/service_name,AlertDefinition/source,AlertDefinition/help_url",
-          Date.now()
-        );
-
-        const alertDefinitionDetails = alertDefinitionResponse.items.find(
-          (item: AlertDefinitionResponseItem) =>
-            item.AlertDefinition.id === parseInt(alertDefinitionId)
+        const alertDefinitionResponse = await AlertsApi.getAlertDefinitionById(
+          clusterName, alertDefinitionId, Date.now(),
         );
+        const alertDefinitionDetails = alertDefinitionResponse?.AlertDefinition ||
+          alertDefinitionResponse?.items?.[0]?.AlertDefinition;
 
         if (alertDefinitionDetails) {
-          // Update local state
-          setLocalRepeatTolerance(
-            alertDefinitionDetails.AlertDefinition.repeat_tolerance
-          );
+          setLocalRepeatTolerance(String(alertDefinitionDetails.repeat_tolerance ?? 1));
           setLocalRepeatToleranceEnabled(
-            alertDefinitionDetails.AlertDefinition.repeat_tolerance_enabled
+            Boolean(alertDefinitionDetails.repeat_tolerance_enabled)
           );
-
-          // Update parent component data
-          alertDefinition.repeat_tolerance =
-            alertDefinitionDetails.AlertDefinition.repeat_tolerance;
-          alertDefinition.repeat_tolerance_enabled =
-            alertDefinitionDetails.AlertDefinition.repeat_tolerance_enabled;
+          onAlertDefinitionUpdate?.({
+            repeat_tolerance: alertDefinitionDetails.repeat_tolerance,
+            repeat_tolerance_enabled: alertDefinitionDetails.repeat_tolerance_enabled,
+          });
         }
       }
     } catch (error) {
@@ -214,8 +189,8 @@ const InformationSection = ({
     // If repeat_tolerance_enabled is true, use repeat_tolerance, otherwise use global default
     const initialValue = localRepeatToleranceEnabled
       ? localRepeatTolerance
-      : parseInt(globalRepeatTolerance);
-    setToleranceValue(initialValue || 1);
+      : globalRepeatTolerance;
+    setToleranceValue(String(initialValue || '1'));
     setToleranceError(null);
     setShowToleranceModal(true);
   };
@@ -226,24 +201,13 @@ const InformationSection = ({
   };
 
   const validateToleranceInput = (value: string | number): boolean => {
-    const stringValue = value.toString();
-    if (stringValue === "DEBUG") {
-      setToleranceError(null);
-      return true;
-    }
-
-    const intValue = parseInt(stringValue, 10);
-    if (isNaN(intValue) || intValue < 1 || intValue > 99) {
-      setToleranceError("Input should be an integer between 1 and 99.");
-      return false;
-    }
-
-    setToleranceError(null);
-    return true;
+    const error = validateRepeatTolerance(value);
+    setToleranceError(error);
+    return error === null;
   };
 
   const handleToleranceInputChange = (value: string) => {
-    setToleranceValue(value as any);
+    setToleranceValue(value);
     validateToleranceInput(value);
   };
 
@@ -264,7 +228,7 @@ const InformationSection = ({
         clusterName,
         alertDefinition.alert_definition_id,
         {
-          "AlertDefinition/repeat_tolerance": toleranceValue.toString(),
+          "AlertDefinition/repeat_tolerance": toleranceValue,
           "AlertDefinition/repeat_tolerance_enabled": enableRepeatTolerance,
         }
       );
@@ -273,10 +237,6 @@ const InformationSection = ({
       setLocalRepeatTolerance(toleranceValue);
       setLocalRepeatToleranceEnabled(enableRepeatTolerance);
 
-      // Update parent component data
-      alertDefinition.repeat_tolerance = toleranceValue;
-      alertDefinition.repeat_tolerance_enabled = enableRepeatTolerance;
-
       if (onAlertDefinitionUpdate) {
         onAlertDefinitionUpdate({
           repeat_tolerance: toleranceValue,
@@ -541,7 +501,7 @@ const InformationSection = ({
                   icon={faUndo}
                   className="text-warning ms-2 cursor-pointer"
                   onClick={() => {
-                    setToleranceValue(parseInt(globalRepeatTolerance) || 1);
+                    setToleranceValue(globalRepeatTolerance || '1');
                     setToleranceError(null);
                   }}
                 />
diff --git a/ambari-web/latest/src/screens/Alerts/MenuBar.tsx b/ambari-web/latest/src/screens/Alerts/MenuBar.tsx
index a16361e168..2093432307 100644
--- a/ambari-web/latest/src/screens/Alerts/MenuBar.tsx
+++ b/ambari-web/latest/src/screens/Alerts/MenuBar.tsx
@@ -18,7 +18,7 @@
 
 import React, { useState, useRef, useEffect } from 'react';
 import { Row, Col, Button } from 'react-bootstrap';
-import { AlertDefinition, AlertGroupItem } from './types';
+import { AlertDefinition, AlertGroupItem, SearchFilter } from './types';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faFilter, faXmarkCircle } from '@fortawesome/free-solid-svg-icons';
 import { ALERT_SEARCH_CATEGORIES } from './constants';
@@ -26,28 +26,22 @@ import { getCategoryValues, getTimeRangeValue } from './alertUtils';
 import '../../styles/MenuBar.scss';
 import { ActionsButton } from './ActionsButton';
 
-interface SearchFilter {
-    category: string;
-    value: string;
-}
-
 interface MenuBarProps {
     title: string;
     alertGroups: AlertGroupItem[];
     alertCounts: Record<string, number>;
     onSearch: (filters: SearchFilter[]) => void;
+    searchFilters: SearchFilter[];
     alertDefinitions?: AlertDefinition[];
     onModalStateChange?: (isOpen: boolean) => void;
-    hasAnyAlertPermissions?: boolean;
 }
 
-const MenuBar: React.FC<MenuBarProps> = ({ title, alertGroups, onSearch,
-                                             alertDefinitions, onModalStateChange, hasAnyAlertPermissions = true
+const MenuBar: React.FC<MenuBarProps> = ({ title, alertGroups, onSearch, searchFilters,
+                                             alertDefinitions, onModalStateChange
     }) => {
-    const [showSearch, setShowSearch] = useState(false);
+    const [showSearch, setShowSearch] = useState(searchFilters.length > 0);
     const [selectedCategory, setSelectedCategory] = useState('');
     const [inputValue, setInputValue] = useState('');
-    const [searchFilters, setSearchFilters] = useState<SearchFilter[]>([]);
     const [suggestions, setSuggestions] = useState<string[]>([]);
     const [showCategories, setShowCategories] = useState(false);
     const searchRef = useRef<HTMLDivElement>(null);
@@ -78,7 +72,6 @@ const MenuBar: React.FC<MenuBarProps> = ({ title, alertGroups, onSearch,
         setInputValue(value);
         if (selectedCategory) {
             const categoryValues = getCategoryValues(selectedCategory, alertGroups || []);
-            console.log('Category values:', categoryValues);
             setSuggestions(
                 categoryValues.filter(item =>
                     item.toLowerCase().includes(value.toLowerCase())
@@ -88,12 +81,10 @@ const MenuBar: React.FC<MenuBarProps> = ({ title, alertGroups, onSearch,
     };
 
     const handleCategorySelect = (category: string) => {
-        console.log('Selected category:', category);
         setSelectedCategory(category);
         setShowCategories(false);
         setInputValue('');
         setSuggestions(getCategoryValues(category, alertGroups || []));
-        console.log('Available suggestions:', getCategoryValues(category, alertGroups || []));
     };
 
     const handleSuggestionSelect = (suggestion: string) => {
@@ -107,7 +98,6 @@ const MenuBar: React.FC<MenuBarProps> = ({ title, alertGroups, onSearch,
             }
             const newFilter = { category: selectedCategory, value: filterValue };
             const newFilters = [...searchFilters, newFilter];
-            setSearchFilters(newFilters);
             setSelectedCategory('');
             setInputValue('');
             setSuggestions([]);
@@ -117,12 +107,10 @@ const MenuBar: React.FC<MenuBarProps> = ({ title, alertGroups, onSearch,
 
     const removeFilter = (index: number) => {
         const newFilters = searchFilters.filter((_, i) => i !== index);
-        setSearchFilters(newFilters);
         onSearch(newFilters);
     };
 
     const clearAllFilters = () => {
-        setSearchFilters([]);
         onSearch([]);
     };
 
@@ -136,14 +124,11 @@ const MenuBar: React.FC<MenuBarProps> = ({ title, alertGroups, onSearch,
                     <Button className="btn btn-default me-2" onClick={toggleSearch}>
                         <FontAwesomeIcon icon={faFilter} />
                     </Button>
-                    {/* Only show Actions button if user has any alert management permissions */}
-                    {hasAnyAlertPermissions && (
-                        <ActionsButton
-                            alertGroups={alertGroups}
-                            allAlertDefinitions={alertDefinitions}
-                            onModalStateChange={onModalStateChange}
-                        />
-                    )}
+                    <ActionsButton
+                        alertGroups={alertGroups}
+                        allAlertDefinitions={alertDefinitions}
+                        onModalStateChange={onModalStateChange}
+                    />
                 </div>
             </div>
             {showSearch && (
diff --git a/ambari-web/latest/src/screens/Alerts/alertUtils.test.ts b/ambari-web/latest/src/screens/Alerts/alertUtils.test.ts
new file mode 100644
index 0000000000..af545e189c
--- /dev/null
+++ b/ambari-web/latest/src/screens/Alerts/alertUtils.test.ts
@@ -0,0 +1,46 @@
+/**
+ * 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 { afterEach, describe, expect, it, vi } from "vitest";
+import type { MergedAlert } from "./types";
+import { filterAlerts, isWithinTimeRange } from "./alertUtils";
+
+const alert = {
+  label: "NameNode Health",
+  serviceDisplayName: "HDFS",
+  component_name: "NAMENODE",
+  state: "Enabled",
+  groups: "HDFS Default, Operators",
+  statuses: [],
+} as unknown as MergedAlert;
+
+describe("alert list filtering", () => {
+  afterEach(() => vi.useRealTimers());
+
+  it("matches one group when a definition belongs to multiple groups", () => {
+    expect(filterAlerts([alert], [{ category: "Group", value: "Operators" }])).toEqual([alert]);
+  });
+
+  it("supports Ambari millisecond and second timestamps in time filters", () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date("2026-08-17T12:00:00Z"));
+    const withinHour = Date.now() - 30 * 60 * 1000;
+    expect(isWithinTimeRange(String(withinHour), "Past 1 hour")).toBe(true);
+    expect(isWithinTimeRange(String(Math.floor(withinHour / 1000)), "Past 1 hour")).toBe(true);
+  });
+});
diff --git a/ambari-web/latest/src/screens/Alerts/alertUtils.ts b/ambari-web/latest/src/screens/Alerts/alertUtils.ts
index 2aa1b10cf3..b15eeef360 100644
--- a/ambari-web/latest/src/screens/Alerts/alertUtils.ts
+++ b/ambari-web/latest/src/screens/Alerts/alertUtils.ts
@@ -39,11 +39,15 @@ export const isWithinTimeRange = (lastStatusChanged: string | null, timeRangeVal
     if (!lastStatusChanged || lastStatusChanged === 'Unknown') return false;
 
     const now = new Date().getTime();
-    const alertTime = new Date(lastStatusChanged).getTime();
+    const normalized = String(lastStatusChanged).trim();
+    const numericTimestamp = /^\d+$/.test(normalized) ? Number(normalized) : NaN;
+    const alertTime = Number.isFinite(numericTimestamp)
+        ? (normalized.length <= 10 ? numericTimestamp * 1000 : numericTimestamp)
+        : new Date(normalized).getTime();
     const hours = TIME_RANGES[timeRangeValue as keyof typeof TIME_RANGES] || 0;
     const timeRange = hours * 60 * 60 * 1000;
 
-    return (now - alertTime) <= timeRange;
+    return Number.isFinite(alertTime) && now >= alertTime && (now - alertTime) <= timeRange;
 };
 
 export const sortAlerts = (alerts: MergedAlert[], sortField: string, isAsc: boolean): MergedAlert[] => {
@@ -87,10 +91,13 @@ export const filterAlerts = (alerts: MergedAlert[], filters: SearchFilter[]): Me
         return filters.every(filter => {
             switch (filter.category) {
                 case 'Alert Definition Name':
-                    return alert.label.toLowerCase().includes(filter.value.toLowerCase());
+                    return (alert.label || '').toLowerCase().includes(filter.value.toLowerCase());
 
                 case 'Service':
-                    return alert.serviceDisplayName.toLowerCase() === filter.value.toLowerCase();
+                    return (alert.serviceDisplayName || '').toLowerCase() === filter.value.toLowerCase();
+
+                case 'Component':
+                    return (alert.component_name || 'N/A').toLowerCase() === filter.value.toLowerCase();
 
                 case 'Status':
                     // Handle empty statuses as NONE
@@ -105,7 +112,7 @@ export const filterAlerts = (alerts: MergedAlert[], filters: SearchFilter[]): Me
                     return alert.statuses[0] && isWithinTimeRange(alert.statuses[0].last_status_changed, filter.value);
 
                 case 'Group':
-                    return alert.groups === filter.value;
+                    return (alert.groups || '').split(',').map((group) => group.trim()).includes(filter.value);
 
                 case 'State':
                     return alert.state === filter.value;
@@ -127,7 +134,6 @@ export const getTimeRangeValue = (range: string): Date | null => {
 
 export const getCategoryValues = (category: string, alertGroups: AlertGroupItem[]): string[] => {
     if (!alertGroups || !Array.isArray(alertGroups) || alertGroups.length === 0) {
-        console.log('No alert groups available for category:', category);
         return [];
     }
 
@@ -150,6 +156,15 @@ export const getCategoryValues = (category: string, alertGroups: AlertGroupItem[
                     return '';
                 }).filter(name => name !== '')
             )));
+        case 'Component':
+            return Array.from(new Set(alertGroups.flatMap(group =>
+                (group.AlertGroup.definitions || []).map(def => {
+                    if (typeof def === 'object' && 'component_name' in def) {
+                        return (def as AlertDefinition).component_name || 'N/A';
+                    }
+                    return '';
+                }).filter(name => name !== '')
+            )));
         case 'Group':
             return Array.from(new Set(alertGroups.map(group =>
                 group.AlertGroup.name + (group.AlertGroup.default ? ' Default' : '')
@@ -261,7 +276,6 @@ export const processData = (
         const definitionId = String(item.definition_id);
         const summary = item.summary;
 
-        // FIXED: Include both regular alerts and maintenance mode alerts
         const statusesWithCounts = Object.keys(summary || {})
             .filter((state) => {
                 const stateData = summary?.[state];
diff --git a/ambari-web/latest/src/screens/Alerts/components/AlertGroupModals.tsx b/ambari-web/latest/src/screens/Alerts/components/AlertGroupModals.tsx
index f783ce3baf..9bf0f184cd 100644
--- a/ambari-web/latest/src/screens/Alerts/components/AlertGroupModals.tsx
+++ b/ambari-web/latest/src/screens/Alerts/components/AlertGroupModals.tsx
@@ -33,6 +33,7 @@ import {
 } from "../types";
 import EditableList from "../../../components/EditableList";
 import Select from "react-select";
+import { validateRepeatTolerance } from '../../../Utils/alertDefinitions';
 
 // Helper function to get definitions from a group
 const getDefinitions = (
@@ -68,22 +69,17 @@ const getGroupNotifications = (
 ): AlertNotification[] => {
   if (!group || !notifications.length) return [];
 
-  const groupId = getGroupId(group);
-  if (!groupId) return [];
+  const targets = "AlertGroup" in group
+    ? group.AlertGroup.targets || []
+    : group.targets || [];
+  const targetIds = new Set(targets.flatMap((target) => {
+    const id = Number(typeof target === "number" ? target : target.id);
+    return Number.isFinite(id) && id > 0 ? [id] : [];
+  }));
 
-  // Filter notifications that are associated with this group
   return notifications.filter((notification) => {
-    const target = notification.AlertTarget;
-
-    // If notification is global, it applies to all groups
-    if (target.global) return true;
-
-    // If notification has specific groups, check if this group is included
-    if (target.groups && Array.isArray(target.groups)) {
-      return target.groups.includes(groupId);
-    }
-
-    return false;
+    const id = Number(notification.AlertTarget.id);
+    return Number.isFinite(id) && targetIds.has(id);
   });
 };
 
@@ -156,6 +152,7 @@ interface ManageAlertGroupsModalProps {
     notifications: AlertNotification[]
   ) => void;
   onManageNotifications?: () => void;
+  onRetry?: () => void;
 }
 
 export const ManageAlertGroupsModal: React.FC<ManageAlertGroupsModalProps> = ({
@@ -179,6 +176,7 @@ export const ManageAlertGroupsModal: React.FC<ManageAlertGroupsModalProps> = ({
   successMessage,
   notifications,
   onNotificationsChange,
+  onRetry,
   // onManageNotifications
 }) => {
   // Filter out deleted groups
@@ -213,7 +211,8 @@ export const ManageAlertGroupsModal: React.FC<ManageAlertGroupsModalProps> = ({
               className="alert alert-danger"
               style={{ marginBottom: "15px", fontWeight: "bold" }}
             >
-              {errorMessage}
+              <span>{errorMessage}</span>
+              {onRetry && <button className="btn btn-outline-danger ms-3" onClick={onRetry}>Retry</button>}
             </div>
           </div>
         </div>
@@ -755,8 +754,8 @@ export const AddDefinitionsModal: React.FC<AddDefinitionsModalProps> = ({
 };
 
 interface ManageAlertSettingsModalProps {
-  alertCheckCount: number | string;
-  setAlertCheckCount: (count: number | string) => void;
+  alertCheckCount: string;
+  setAlertCheckCount: (count: string) => void;
   setIsSaveDisabled: (disabled: boolean) => void;
   modalError: string;
   modalSuccess: string;
@@ -774,19 +773,9 @@ export const ManageAlertSettingsModal: React.FC<
   const [validationError, setValidationError] = React.useState<string>("");
 
   const validateInput = (value: string): boolean => {
-    if (value === "DEBUG") {
-      setValidationError("");
-      return true;
-    }
-
-    const intValue = parseInt(value, 10);
-    if (isNaN(intValue) || intValue < 1 || intValue > 99) {
-      setValidationError("Input should be an integer between 1 and 99.");
-      return false;
-    }
-
-    setValidationError("");
-    return true;
+    const error = validateRepeatTolerance(value);
+    setValidationError(error || "");
+    return error === null;
   };
 
   const handleInputChange = (value: string) => {
diff --git a/ambari-web/latest/src/screens/Alerts/components/NotificationModals.tsx b/ambari-web/latest/src/screens/Alerts/components/NotificationModals.tsx
index b6bd7284f7..496dc2e75c 100644
--- a/ambari-web/latest/src/screens/Alerts/components/NotificationModals.tsx
+++ b/ambari-web/latest/src/screens/Alerts/components/NotificationModals.tsx
@@ -20,6 +20,7 @@ import React, { useState } from 'react';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faPlus, faMinus, faGear } from '@fortawesome/free-solid-svg-icons';
 import { AlertNotification, AlertTarget } from '../types';
+import { notificationTypeToUi } from '../../../Utils/alertNotifications';
 
 interface ManageNotificationsModalProps {
   notifications: AlertNotification[];
@@ -30,7 +31,7 @@ interface ManageNotificationsModalProps {
   isDropdownOpen: boolean;
   handleEdit: (notification: AlertNotification) => void;
   handleDuplicate: (notification: AlertNotification) => void;
-  handleToggleEnabled: (notification: AlertNotification) => void;
+  handleToggleEnabled: (notification: AlertNotification) => Promise<void> | void;
   handleCreateNotification: () => void;
   errorMessage: string;
   successMessage: string;
@@ -38,6 +39,7 @@ interface ManageNotificationsModalProps {
   setSuccessMessage: (message: string) => void;
   onFormValidityChange?: (isValid: boolean) => void;
   alertGroups?: { id: number; name: string; default?: boolean }[];
+  onRetry?: () => void;
 }
 
 export const ManageNotificationsModal: React.FC<ManageNotificationsModalProps> = ({
@@ -54,6 +56,7 @@ export const ManageNotificationsModal: React.FC<ManageNotificationsModalProps> =
   errorMessage,
   successMessage,
   alertGroups = [],
+  onRetry,
 }) => {
   // Helper function to safely display groups
   const displayGroups = (target: AlertTarget): string => {
@@ -96,36 +99,9 @@ export const ManageNotificationsModal: React.FC<ManageNotificationsModalProps> =
     return String(recipients);
   };
 
-  // Add new state for tracking modified notifications
-  const [modifiedNotifications, setModifiedNotifications] = useState<Record<string, AlertNotification>>({});
-  const [_isLoading, _setIsLoading] = useState(false);
-
-  // Local handleToggleEnabled to handle immediate UI updates
   const handleToggleEnabled = (notification: AlertNotification) => {
     if (!notification || !notification.AlertTarget) return;
-
-    // Call the parent's toggle function to update the main notifications state immediately
-    parentHandleToggleEnabled(notification);
-
-    const updatedNotification = {
-      ...notification,
-      AlertTarget: {
-        ...notification.AlertTarget,
-        is_enabled: !notification.AlertTarget.is_enabled,
-        _isModified: true
-      }
-    };
-
-    // Update the modified notifications map
-    setModifiedNotifications({
-      ...modifiedNotifications,
-      [String(notification.AlertTarget.id)]: updatedNotification
-    });
-
-    // If this is the selected notification, update it too
-    if (selectedNotification && selectedNotification.AlertTarget.id === notification.AlertTarget.id) {
-      handleSelectNotification(updatedNotification);
-    }
+    void parentHandleToggleEnabled(notification);
   };
 
 
@@ -147,7 +123,8 @@ export const ManageNotificationsModal: React.FC<ManageNotificationsModalProps> =
 
       {errorMessage && (
         <div className="alert alert-danger">
-          {errorMessage}
+          <span>{errorMessage}</span>
+          {onRetry && <button className="btn btn-outline-danger ms-3" onClick={onRetry}>Retry</button>}
         </div>
       )}
 
@@ -270,7 +247,7 @@ export const ManageNotificationsModal: React.FC<ManageNotificationsModalProps> =
                     </div>
                     <div className="row mt-3">
                       <div className="col-md-3 input-label">Type</div>
-                      <div className="col-md-9 input-value">{selectedNotification.AlertTarget?.notification_type}</div>
+                      <div className="col-md-9 input-value">{notificationTypeToUi(selectedNotification.AlertTarget?.notification_type)}</div>
                     </div>
                     {selectedNotification.AlertTarget?.notification_type === 'EMAIL' && (
                       <div className="row mt-3">
@@ -307,10 +284,12 @@ export const ManageNotificationsModal: React.FC<ManageNotificationsModalProps> =
 
 interface DeleteNotificationModalProps {
   notification: AlertNotification | null;
+  errorMessage?: string;
 }
 
 export const DeleteNotificationModal: React.FC<DeleteNotificationModalProps> = ({
-  notification
+  notification,
+  errorMessage,
 }) => {
   if (!notification) {
     return <div>No notification selected</div>;
@@ -323,6 +302,7 @@ export const DeleteNotificationModal: React.FC<DeleteNotificationModalProps> = (
         <strong>{notification.AlertTarget.name}</strong>?
       </p>
       <p>This action cannot be undone.</p>
+      {errorMessage && <div className="alert alert-danger">{errorMessage}</div>}
     </div>
   );
 };
@@ -381,6 +361,7 @@ interface NotificationFormModalProps {
   successMessage: string;
   setErrorMessage: (message: string) => void;
   setSuccessMessage: (message: string) => void;
+  preserveSensitivePassword?: boolean;
 }
 
 export const NotificationFormModal: React.FC<NotificationFormModalProps> = ({
@@ -434,6 +415,7 @@ export const NotificationFormModal: React.FC<NotificationFormModalProps> = ({
   errorMessage,
   successMessage,
   setErrorMessage,
+  preserveSensitivePassword = false,
 }) => {
   const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
   const [showAddPropertyModal, setShowAddPropertyModal] = useState(false);
@@ -519,7 +501,7 @@ export const NotificationFormModal: React.FC<NotificationFormModalProps> = ({
 
       case 'password':
         // Ember smtpPasswordValidation logic
-        if (notificationType === 'EMAIL' && useAuthentication && !value.trim()) {
+        if (notificationType === 'EMAIL' && useAuthentication && !preserveSensitivePassword && !value.trim()) {
           return 'Password is required';
         }
         return '';
@@ -586,7 +568,7 @@ export const NotificationFormModal: React.FC<NotificationFormModalProps> = ({
       if (!recipients.trim()) return false;
       if (useAuthentication) {
         if (!username.trim()) return false;
-        if (!password.trim()) return false;
+        if (!preserveSensitivePassword && !password.trim()) return false;
         if (password !== passwordConfirmation) return false;
       }
     } else if (notificationType === 'SNMP' || notificationType === 'Custom SNMP') {
@@ -662,7 +644,7 @@ export const NotificationFormModal: React.FC<NotificationFormModalProps> = ({
     }
   }, [name, notificationType, recipients, emailFrom, smtpPort, username, password, passwordConfirmation, 
       useAuthentication, snmpHosts, snmpPort, snmpCommunity, scriptFilename, groupsOption, 
-      selectedGroups, selectedSeverities]);
+      selectedGroups, selectedSeverities, preserveSensitivePassword]);
 
   // Add custom property with proper validation
   const handleAddCustomProperty = () => {
diff --git a/ambari-web/latest/src/screens/Alerts/constants.ts b/ambari-web/latest/src/screens/Alerts/constants.ts
index f8326c6ae6..92bab68cfa 100644
--- a/ambari-web/latest/src/screens/Alerts/constants.ts
+++ b/ambari-web/latest/src/screens/Alerts/constants.ts
@@ -24,6 +24,7 @@ export const ALERT_SEARCH_CATEGORIES = [
     'Status',
     'Alert Definition Name',
     'Service',
+    'Component',
     'Last Status Changed',
     'State',
     'Group',
@@ -43,4 +44,4 @@ export const STATUS_OPTIONS = [
     AlertStatus.WARNING,
     AlertStatus.CRITICAL,
     AlertStatus.UNKNOWN
-] as const;
\ No newline at end of file
+] as const;
diff --git a/ambari-web/latest/src/screens/Alerts/types.ts b/ambari-web/latest/src/screens/Alerts/types.ts
index 8276961aaf..986c8c213c 100644
--- a/ambari-web/latest/src/screens/Alerts/types.ts
+++ b/ambari-web/latest/src/screens/Alerts/types.ts
@@ -39,7 +39,7 @@ export interface AlertGroup {
     cluster_name: string;
     name: string;
     id: number;
-    targets?: AlertTarget[];
+    targets?: Array<number | AlertTarget>;
     _deleted?: boolean;
     _isModified?: boolean;
     _isNew?: boolean;
@@ -54,7 +54,7 @@ export interface AlertGroupState {
     name: string;
     default: boolean;
     definitions: AlertDefinitionReference[];
-    targets: AlertTarget[];
+    targets: Array<number | AlertTarget>;
     _isModified?: boolean;
     _isNew?: boolean;
     _deleted?: boolean;
@@ -127,7 +127,7 @@ export interface MergedAlert {
     component_name: string;
     alert_definition_id: number;
     source_type: string;
-    statuses: { status: AlertStatus; count: number; last_status_changed: string | null; latest_text: string }[];
+    statuses: { status: AlertStatus; count: number; maintenance_count?: number; last_status_changed: string | null; latest_text: string }[];
     last_status_changed: string | null;
     lastTriggeredFormatted: string;
     lastTriggeredAgoFormatted: string;
@@ -142,6 +142,11 @@ export interface MergedAlert {
     maintenance_state?: string;
 }
 
+export interface AlertEditorHandle {
+    save: () => Promise<boolean>;
+    discard: () => void;
+}
+
 export interface SummaryData {
     last_status_changed: string | null;
     statuses: { last_status_changed: string | null; count: number; latest_text: string; status: AlertStatus }[];
diff --git a/ambari-web/latest/src/screens/Alerts/utils/alertGroupUtils.ts b/ambari-web/latest/src/screens/Alerts/utils/alertGroupUtils.ts
index 1c00f86588..f061b9f0cf 100644
--- a/ambari-web/latest/src/screens/Alerts/utils/alertGroupUtils.ts
+++ b/ambari-web/latest/src/screens/Alerts/utils/alertGroupUtils.ts
@@ -18,7 +18,6 @@
 
 import { AlertGroupItem, AlertDefinition } from '../types';
 import { AlertsApi } from '../../../api/alertsApi';
-import { ambariApi } from '../../../api/config/axiosConfig';
 
 /**
  * Check if a group has been modified compared to the original
@@ -39,11 +38,9 @@ export const isGroupModified = (group: AlertGroupItem, originalGroups: AlertGrou
  */
 export const fetchAlertGroups = async (clusterName: string) => {
   if (!clusterName) {
-    console.log('Skipping alert groups fetch - no cluster name');
     throw new Error('Cluster name is required');
   }
 
-  console.log('Fetching alert groups for cluster:', clusterName);
   const response = await AlertsApi.getAlertGroups(clusterName);
 
   if (response && response.items) {
@@ -75,7 +72,6 @@ export const fetchAlertDefinitions = async (clusterName: string) => {
     throw new Error('Cluster name missing. Please ensure you are viewing alerts for a specific cluster.');
   }
 
-  console.log('Fetching alert definitions for cluster:', clusterName);
   const response = await AlertsApi.getAlertDefinition(
     clusterName,
     'AlertDefinition/component_name,AlertDefinition/description,AlertDefinition/enabled,AlertDefinition/id,AlertDefinition/label,AlertDefinition/name,AlertDefinition/service_name',
@@ -102,11 +98,17 @@ export const fetchAlertDefinitions = async (clusterName: string) => {
 /**
  * Create a new alert group
  */
-export const createAlertGroup = async (clusterName: string, name: string, definitions: number[] = []) => {
+export const createAlertGroup = async (
+  clusterName: string,
+  name: string,
+  definitions: number[] = [],
+  targets: number[] = [],
+) => {
   const payload = {
     AlertGroup: {
       name,
-      definitions
+      definitions,
+      targets,
     }
   };
   
@@ -117,33 +119,8 @@ export const createAlertGroup = async (clusterName: string, name: string, defini
  * Update an existing alert group
  */
 export const updateAlertGroup = async (clusterName: string, groupId: number, name: string, definitions: number[] = [], targets: number[] = []) => {
-  // Create a completely new object with ONLY the required fields
-  const alertGroupData: any = {
-    name
-  };
-
-  // Only include definitions if there are any
-  if (definitions && definitions.length > 0) {
-    alertGroupData.definitions = definitions;
-  }
-
-  // Only include targets if there are any
-  if (targets && targets.length > 0) {
-    alertGroupData.targets = targets;
-  }
-
-  const requestBody = {
-    AlertGroup: alertGroupData
-  };
-
-  console.log(`Updating group ${groupId} with payload:`, JSON.stringify(requestBody));
-
-  // Make the PUT request directly
-  const url = `clusters/${clusterName}/alert_groups/${groupId}`;
-  return await ambariApi.request({
-    url: url,
-    method: "PUT",
-    data: requestBody
+  return await AlertsApi.updateAlertGroup(clusterName, groupId, {
+    AlertGroup: { name, definitions, targets },
   });
 };
 
@@ -218,8 +195,6 @@ export const filterAvailableDefinitions = (
     }).filter(id => id !== null)
   );
   
-  console.log('Existing definition IDs:', [...existingIds]);
-  
   // Return definitions not already in the group
   return allDefinitions.filter(def => !existingIds.has(def.id));
 };
diff --git a/docs/frontend-refactor/ember-baseline/05-alerts.md b/docs/frontend-refactor/ember-baseline/05-alerts.md
index 5ea4b2b63d..b8d6e20d7a 100644
--- a/docs/frontend-refactor/ember-baseline/05-alerts.md
+++ b/docs/frontend-refactor/ember-baseline/05-alerts.md
@@ -38,7 +38,7 @@ The entry point is `/main/alerts`, details are at `/main/alerts/:alert_definitio
 | ALERT-DEF-003 | Edits general alert configs/thresholds | Validates warning/critical order, values, units, and other fields. Save first disables the fields and exits edit, then sends a PUT without a custom error callback; the label Save-on-leave and this operation are two independent concurrent requests with no result aggregation | `alerts.update_alert_definition` | `app/controllers/main/alerts/definition_configs_controller.js#saveConfigs`, `definition_details_controller.js#saveLabelAnd [...]
 | ALERT-DEF-004 | Enables/disables a definition | The details entry requires `SERVICE.TOGGLE_ALERTS` and confirmation; as in the list, enabled changes optimistically and failure has no rollback | `alerts.update_alert_definition` | definition details template, `app/controllers/main/alerts/definition_details_controller.js#toggleDefinitionState` |
 | ALERT-DEF-005 | Edits repeat tolerance/check count; accepts 1 through 99 or the hidden sentinel `DEBUG`, and can disable repeat tolerance | Enabled state and tolerance are submitted through two independent PUT requests. The popup closes immediately after submission with no aggregation/rollback, so partial success is possible | `alerts.update_alert_definition` | `app/controllers/main/alerts/definition_details_controller.js#editRepeatTolerance` |
-| ALERT-DEF-006 | Deletes a custom definition | Model properties control whether default/stack definitions can be deleted; confirmation is required and the current page remains on failure | `alerts.delete_alert_definition` | definition details controller |
+| ALERT-DEF-006 | Contains an unreachable controller method for deleting a definition; this is `BROKEN/DEAD_CODE`, not a user-facing feature | `deleteAlertDefinition` and its AJAX definition exist, but no inspected Classic template, view, route, menu, or action invokes the method. React must not add deletion UI solely to match this dead code | `alerts.delete_alert_definition` is defined but has no reachable UI call site | `app/controllers/main/alerts/definition_details_controller.js#dele [...]
 | ALERT-DEF-007 | Lists current instances, displaying service/host, state, last check, and response; can navigate to service or host alerts | Instance polling stops when the page exits | `alerts.instances.by_definition` | alert instances controller, details template |
 | ALERT-DEF-008 | Opens instance response/log text | The modal displays the `text` carried by the instance, supports Copy and opening in a new window, makes no new backend log request, and provides no download | No new request; uses data from the instances response | details/instance views, `app/views/common/modal_popups/logs_popup.js` |
 | ALERT-DEF-009 | Queries instance history from the last 24 hours and displays returned record counts by host | This is not a state-change timeline; the UI displays a history count for each host | `alerts.get_instances_history` | `app/controllers/main/alerts/definition_details_controller.js`, definition details template/model |
@@ -48,8 +48,8 @@ The entry point is `/main/alerts`, details are at `/main/alerts/:alert_definitio
 
 | ID | Step/behavior | Preconditions/boundaries | Backend requests |
 | --- | --- | --- | --- |
-| ALERT-CREATE-001 | Step 1 Choose Alert Type | Port, Web, Script, and Aggregate enter their corresponding configurations; Metric parameters are `OUT_OF_SCOPE`. Although the fixture lists Raw, the renderer has no `RAW` case and accesses a missing source on continuation; mark it `BROKEN/PLACEHOLDER` | None |
-| ALERT-CREATE-002 | Step 2 Define Alert and Thresholds | Enters and validates name/label, service/component/scope, interval/timeout, and type-specific source, warning/critical/retry, and other fields in the shared configs view; Next generates the request JSON in this step | Stack/definitions load |
+| ALERT-CREATE-001 | Step 1 Choose Alert Type | Port and Web enter functional-looking configuration paths. Metric parameters are `OUT_OF_SCOPE`. Raw has no renderer case and is `BROKEN/PLACEHOLDER`; Script and Aggregate enter Step 2 but their inspected data paths are broken as described in `ALERT-CREATE-002` | None |
+| ALERT-CREATE-002 | Step 2 Define Alert and Thresholds | Port and Web render the common name/service/component/scope fields and their type-specific source fields. Script dereferences `content.parameters` even though the wizard content only contains `selectedType`. Aggregate renders no common definition fields and Step 2 continuation dereferences the absent definition name. Script and Aggregate are therefore `BROKEN` in inspected Classic source. A React repair may be documented as `IMPRO [...]
 | ALERT-CREATE-003 | Step 3 Review | Read-only display of the selected type and formatted Alert Definition JSON; thresholds are no longer edited in this step and submission occurs only on Done | No mutation |
 | ALERT-CREATE-004 | Done creates the definition and returns to Alerts | The menu requires `supports.createAlerts`, while the wizard route additionally requires `SERVICE.TOGGLE_ALERTS`; when the server fails, the create promise does not complete the subsequent close/finish flow | `alerts.create_alert_definition` |
 | ALERT-CREATE-005 | Wizard forward/back navigation | Completed steps can be revisited; navigation forward is blocked when validation is not satisfied | None |
@@ -58,9 +58,9 @@ The entry point is `/main/alerts`, details are at `/main/alerts/:alert_definitio
 
 | ID | Function and behavior | Preconditions/boundaries | Backend requests | Primary evidence |
 | --- | --- | --- | --- | --- |
-| ALERT-GROUP-001 | Lists groups, descriptions, definitions, and notification targets by service | The Manage Groups action is visible | `alerts.load_alert_groups`, `alerts.notifications` | manage alert groups controller/template |
+| ALERT-GROUP-001 | Lists groups, definitions, and notification targets by service | The Manage Groups action is visible; the inspected Classic group contract has no description field | `alerts.load_alert_groups`, `alerts.notifications` | manage alert groups controller/template |
 | ALERT-GROUP-002 | Creates a group | Name is required and must be unique within the service | `alert_groups.create` | manage alert groups controller |
-| ALERT-GROUP-003 | Renames, edits the description of, or copies a group | Definitions in the default group cannot be modified as for an ordinary group | `alert_groups.update` | manage alert groups controller |
+| ALERT-GROUP-003 | Renames or copies a group | Definitions in the default group cannot be modified as for an ordinary group; the inspected Classic group contract has no description field | `alert_groups.update` or create for a copy | manage alert groups controller |
 | ALERT-GROUP-004 | Adds/removes definitions from a group, with service/component filtering and multi-select | Manual additions/removals are prohibited for the default group; at least one definition must be selected | `alert_groups.update` | manage alert groups controller, add definition popup |
 | ALERT-GROUP-005 | Associates or disassociates notification targets with a group | The notification must already exist | `alert_groups.update` | manage alert groups controller |
 | ALERT-GROUP-006 | Deletes a non-default group | The default group cannot be deleted; confirmation is required | `alert_groups.delete` | manage alert groups controller |
diff --git a/docs/frontend-refactor/ember-baseline/15-five-pass-audit.md b/docs/frontend-refactor/ember-baseline/15-five-pass-audit.md
index 3fb665b7d7..a43fd71cee 100644
--- a/docs/frontend-refactor/ember-baseline/15-five-pass-audit.md
+++ b/docs/frontend-refactor/ember-baseline/15-five-pass-audit.md
@@ -29,7 +29,7 @@ Here, "passing" means only that generated artifacts, source locations, and the a
 | 2. Template-action and JavaScript-behavior reverse audit | 299 distinct Handlebars action names and 587 template occurrences; then manually traced view clicks, controller methods, observers, timers, dynamic actions, and route lifecycles | Host Re-upgrade has a static UI/controller path, but its request name is unregistered, its state was removed from the Server enumeration, and its payload still hard-codes the old stack; `App.ajax.send` only warns and returns `null` for an unregistered [...]
 | 3. Network and API reverse audit | 288 included non-Metrics named AJAX definitions and 95 excluded Metrics definitions; 394 `App.ajax.send` call sites consisted of 364 registered, 27 dynamic, and 3 unregistered calls. Of the 27 dynamic call sites, 23 were closed-set and 4 were open-boundary cases; all 45 unique candidate names were registered. All registry HTTP methods were actually static, so 0 dynamic methods should be extracted; 3 included and 1 excluded Metrics definition also prov [...]
 | 4. Permission, flag, state, and recovery reverse audit | 38 permission names and 147 static uses, including 130 `isAuthorized` and 17 `havePermissions` uses; 23 `App.supports` flags and 58 uses; also manually indexed the three runtime gates `App.stackVersionsAvailable`, `App.upgradeHistoryAvailable`, and `App.enableDigitalClock`. Rechecked state, owner, persistence, Retry, Skip, and rollback across installation, Kerberos, NNHA/JN, RM/RA, and Federation/HAWQ | Comma-separated permission [...]
-| 5. Test and evidence-consistency reverse audit | `classic/test` contains 546 JS files and 500 `_test.js` files. The manifest has 499 entries: 498 `_test` references, 497 unique references, and 1 initialization module; `test/utils/config_test` is duplicated. 52 files contain 81 skip markers: 59 `describe.skip` plus 22 `it.skip`. Modules `01` through `13` ultimately contain 1002 unique stable IDs; authored module tables contain 282 full source-file references, normalized to 125 unique pa [...]
+| 5. Test and evidence-consistency reverse audit | `classic/test` contains 546 JS files and 500 `_test.js` files. The manifest has 499 entries: 498 `_test` references, 497 unique references, and 1 initialization module; `test/utils/config_test` is duplicated. 52 files contain 81 skip markers: 59 `describe.skip` plus 22 `it.skip`. Modules `01` through `13` ultimately contain 1002 unique stable IDs; authored module tables contain 283 full source-file references, normalized to 125 unique pa [...]
 
 ## Frozen Conclusions
 
@@ -83,7 +83,7 @@ The validator compares the following JSON field by field with the current genera
   "realtimeUnsubscribeSites": 1,
   "realtimeLocationOccurrences": 142,
   "realtimeUniqueLocations": 131,
-  "sourceReferenceOccurrences": 282,
+  "sourceReferenceOccurrences": 283,
   "sourceReferenceUniquePaths": 125,
   "testJsFiles": 546,
   "diskTestModules": 500,
diff --git a/docs/frontend-refactor/ember-baseline/generated/feature-index.json b/docs/frontend-refactor/ember-baseline/generated/feature-index.json
index ef28030444..bc9e68bc18 100644
--- a/docs/frontend-refactor/ember-baseline/generated/feature-index.json
+++ b/docs/frontend-refactor/ember-baseline/generated/feature-index.json
@@ -1086,7 +1086,7 @@
   },
   {
     "id": "ALERT-DEF-006",
-    "summary": "Deletes a custom definition",
+    "summary": "Contains an unreachable controller method for deleting a definition; this is `BROKEN/DEAD_CODE`, not a user-facing feature",
     "section": "Definition Details and Instances",
     "moduleFile": "05-alerts.md",
     "line": 41
@@ -1156,7 +1156,7 @@
   },
   {
     "id": "ALERT-GROUP-001",
-    "summary": "Lists groups, descriptions, definitions, and notification targets by service",
+    "summary": "Lists groups, definitions, and notification targets by service",
     "section": "Alert Groups",
     "moduleFile": "05-alerts.md",
     "line": 61
@@ -1170,7 +1170,7 @@
   },
   {
     "id": "ALERT-GROUP-003",
-    "summary": "Renames, edits the description of, or copies a group",
+    "summary": "Renames or copies a group",
     "section": "Alert Groups",
     "moduleFile": "05-alerts.md",
     "line": 63
diff --git a/docs/frontend-refactor/ember-baseline/generated/feature-index.md b/docs/frontend-refactor/ember-baseline/generated/feature-index.md
index 0762278f3c..442fd29096 100644
--- a/docs/frontend-refactor/ember-baseline/generated/feature-index.md
+++ b/docs/frontend-refactor/ember-baseline/generated/feature-index.md
@@ -178,7 +178,7 @@
 | `ALERT-DEF-003` | `05-alerts.md` | Definition Details and Instances | Edits general alert configs/thresholds | [source](../05-alerts.md#L38) |
 | `ALERT-DEF-004` | `05-alerts.md` | Definition Details and Instances | Enables/disables a definition | [source](../05-alerts.md#L39) |
 | `ALERT-DEF-005` | `05-alerts.md` | Definition Details and Instances | Edits repeat tolerance/check count; accepts 1 through 99 or the hidden sentinel \`DEBUG\`, and can disable repeat tolerance | [source](../05-alerts.md#L40) |
-| `ALERT-DEF-006` | `05-alerts.md` | Definition Details and Instances | Deletes a custom definition | [source](../05-alerts.md#L41) |
+| `ALERT-DEF-006` | `05-alerts.md` | Definition Details and Instances | Contains an unreachable controller method for deleting a definition; this is \`BROKEN/DEAD_CODE\`, not a user-facing feature | [source](../05-alerts.md#L41) |
 | `ALERT-DEF-007` | `05-alerts.md` | Definition Details and Instances | Lists current instances, displaying service/host, state, last check, and response; can navigate to service or host alerts | [source](../05-alerts.md#L42) |
 | `ALERT-DEF-008` | `05-alerts.md` | Definition Details and Instances | Opens instance response/log text | [source](../05-alerts.md#L43) |
 | `ALERT-DEF-009` | `05-alerts.md` | Definition Details and Instances | Queries instance history from the last 24 hours and displays returned record counts by host | [source](../05-alerts.md#L44) |
@@ -188,9 +188,9 @@
 | `ALERT-CREATE-003` | `05-alerts.md` | Create Alert Definition | Step 3 Review | [source](../05-alerts.md#L53) |
 | `ALERT-CREATE-004` | `05-alerts.md` | Create Alert Definition | Done creates the definition and returns to Alerts | [source](../05-alerts.md#L54) |
 | `ALERT-CREATE-005` | `05-alerts.md` | Create Alert Definition | Wizard forward/back navigation | [source](../05-alerts.md#L55) |
-| `ALERT-GROUP-001` | `05-alerts.md` | Alert Groups | Lists groups, descriptions, definitions, and notification targets by service | [source](../05-alerts.md#L61) |
+| `ALERT-GROUP-001` | `05-alerts.md` | Alert Groups | Lists groups, definitions, and notification targets by service | [source](../05-alerts.md#L61) |
 | `ALERT-GROUP-002` | `05-alerts.md` | Alert Groups | Creates a group | [source](../05-alerts.md#L62) |
-| `ALERT-GROUP-003` | `05-alerts.md` | Alert Groups | Renames, edits the description of, or copies a group | [source](../05-alerts.md#L63) |
+| `ALERT-GROUP-003` | `05-alerts.md` | Alert Groups | Renames or copies a group | [source](../05-alerts.md#L63) |
 | `ALERT-GROUP-004` | `05-alerts.md` | Alert Groups | Adds/removes definitions from a group, with service/component filtering and multi-select | [source](../05-alerts.md#L64) |
 | `ALERT-GROUP-005` | `05-alerts.md` | Alert Groups | Associates or disassociates notification targets with a group | [source](../05-alerts.md#L65) |
 | `ALERT-GROUP-006` | `05-alerts.md` | Alert Groups | Deletes a non-default group | [source](../05-alerts.md#L66) |
diff --git a/docs/frontend-refactor/react-current/05-alerts-gap.md b/docs/frontend-refactor/react-current/05-alerts-gap.md
new file mode 100644
index 0000000000..3bfcc5e182
--- /dev/null
+++ b/docs/frontend-refactor/react-current/05-alerts-gap.md
@@ -0,0 +1,240 @@
+<!---
+   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.
+-->
+
+# React Alerts Comparison
+
+## Comparison Scope
+
+| Item | Value |
+| --- | --- |
+| Ember baseline | `ember-baseline/05-alerts.md` |
+| React implementation | `ambari-web/latest`, Module 05 audit based on `4fb6dadf19` plus the Alerts remediation branch |
+| Tracking issue | `AMBARI-26631` |
+| Feature IDs | 40 non-Metrics IDs from `ALERT-LIST-001` through `ALERT-SET-003` |
+| Review date | 2026-08-17 |
+| Metrics boundary | Metric Alert Definition parameters, metric expressions, metric data, and Metrics APIs are excluded |
+
+The audit compared the written baseline with executable Classic source and the current React implementation. It also reverse-scanned the generated AJAX definitions and call sites, direct HTTP calls, browser entry points, realtime channels, routes, permissions, and feature flags. The heuristic generated module inventory was used only as a candidate list.
+
+Static inspection does not establish complete parity. Every feature that becomes `STATICALLY_ALIGNED` remains represented in the live runtime acceptance matrix until it has been exercised against Ambari Server.
+
+## Initial Static Conclusion
+
+| Status | Count |
+| --- | ---: |
+| `PARTIAL` | 21 |
+| `INCORRECT` | 12 |
+| `MISSING` | 6 |
+| `BROKEN_LEGACY` | 1 |
+| Total | 40 |
+
+`BROKEN_LEGACY` records unreachable or broken Classic behavior that React must not reproduce merely to make a checklist look complete. Script and Aggregate creation may intentionally repair broken Classic paths and then be marked `IMPROVED_BEYOND_CLASSIC`.
+
+## Post-Implementation Static Conclusion
+
+| Status | Count |
+| --- | ---: |
+| `STATICALLY_ALIGNED` | 37 |
+| `IMPROVED_BEYOND_CLASSIC` | 1 |
+| `BROKEN_LEGACY` | 1 |
+| `LIVE_REQUIRED` | 1 |
+| Total | 40 |
+
+These statuses describe source and focused-test evidence only. They do not change any row in the live runtime acceptance matrix.
+
+## Baseline Corrections
+
+The five-pass audit found three places where the written baseline overstated executable Classic behavior:
+
+1. `ALERT-DEF-006`: `deleteAlertDefinition` and `alerts.delete_alert_definition` exist, but no inspected Classic template, view, route, action, or menu invokes the controller method. It is unreachable dead code, not a deletion workflow.
+2. `ALERT-CREATE-001/002`: Port and Web have functional-looking configuration paths. RAW has no renderer case. Script dereferences `content.parameters`, which the wizard does not populate. Aggregate omits the common definition fields and continuation dereferences a missing definition name.
+3. `ALERT-GROUP-001/003`: the inspected Classic Alert Group UI and request contract contain name, definitions, and notification targets, but no description field.
+
+The Ember baseline was corrected to preserve these boundaries. React must not invent definition deletion or group descriptions for parity. A working Script or Aggregate creator is a deliberate repair beyond Classic.
+
+## Post-Implementation Feature Status
+
+### Definition List and Entry Points
+
+| ID | Static status | React implementation evidence | Remaining live evidence |
+| --- | --- | --- | --- |
+| `ALERT-LIST-001` | `STATICALLY_ALIGNED` | Group definitions and grouped summaries merge by definition ID; list and definition loads have separate recoverable errors; 30-second polling retains prior rows on failure. | Confirm Server field completeness and polling convergence. |
+| `ALERT-LIST-002` | `STATICALLY_ALIGNED` | Filtering and full-result sorting run after every data refresh; group membership handles multiple groups; numeric second/millisecond time filters work; filter/sort state persists per cluster in `sessionStorage`. | Confirm page, filter, and sort retention in a browser through polling and navigation. |
+| `ALERT-LIST-003` | `STATICALLY_ALIGNED` | Definition, host, and service links use backend identifiers; persisted list state restores after returning from details. | Exercise every Classic popup and return path. |
+| `ALERT-LIST-004` | `STATICALLY_ALIGNED` | List toggles use `CLUSTER.TOGGLE_ALERTS`, await the PUT, show failure in the confirmation modal, and refresh only after success. | Run the cluster/service permission-role matrix. |
+| `ALERT-LIST-005` | `STATICALLY_ALIGNED` | Groups and Settings remain visible; Notifications uses `CLUSTER.MANAGE_ALERT_NOTIFICATIONS`; Create uses `supports.createAlerts`; direct creation is separately route-protected. | Confirm feature-flag, permission, and upgrade-state combinations. |
+
+### Definition Details and Instances
+
+| ID | Static status | React implementation evidence | Remaining live evidence |
+| --- | --- | --- | --- |
+| `ALERT-DEF-001` | `STATICALLY_ALIGNED` | Details load the exact definition resource by ID and independently merge groups and summaries. | Open a definition with only default or empty non-default membership. |
+| `ALERT-DEF-002` | `STATICALLY_ALIGNED` | Label validation precedes one awaited PUT; failed values remain in edit mode for Retry. | Inject a Server rejection and retry in-browser. |
+| `ALERT-DEF-003` | `STATICALLY_ALIGNED` | Update construction preserves raw source data, description, interval, thresholds, and parameters; invalid numeric/threshold values block Save. | Validate every installed non-Metric source shape against Server. |
+| `ALERT-DEF-004` | `STATICALLY_ALIGNED` | Detail toggles use `SERVICE.TOGGLE_ALERTS`, update UI only after success, and retain an error in the modal on failure. | Confirm Server-converged state and upgrade boundaries. |
+| `ALERT-DEF-005` | `STATICALLY_ALIGNED` | Per-definition repeat tolerance accepts 1-99 and `DEBUG`, uses one definition PUT, and retains the dialog on failure. | Round-trip all boundary values against Server. |
+| `ALERT-DEF-006` | `BROKEN_LEGACY` | React has no deletion UI, which matches the reachable Classic surface. Classic's controller method and AJAX definition are dead code. | Do not add deletion UI solely for parity; revisit only with a separately specified product requirement. |
+| `ALERT-DEF-007` | `STATICALLY_ALIGNED` | Definition-specific polling stops on unmount, keeps stale data on refresh failure, recovers from an initial failure, and separates core-instance and history errors. | Verify entry/exit timers and transient failures in a real browser. |
+| `ALERT-DEF-008` | `STATICALLY_ALIGNED` | Copy and new-window rendering use literal text nodes and never `document.write`. | Probe markup and script-like Server text under the deployed CSP. |
+| `ALERT-DEF-009` | `STATICALLY_ALIGNED` | The exact 24-hour history resource is queried and records are counted per host before sorting and pagination. | Verify timestamp boundaries and multi-host counts. |
+| `ALERT-DEF-010` | `STATICALLY_ALIGNED` | `useBlocker` and `beforeunload` protect dirty label/config editors; Save awaits both, Discard restores both, and a partial failure leaves the failed editor dirty. | Exercise browser Back, refresh, close, and all combined-editor outcomes. |
+
+### Create Alert Definition
+
+| ID | Static status | React implementation evidence | Remaining live evidence |
+| --- | --- | --- | --- |
+| `ALERT-CREATE-001` | `STATICALLY_ALIGNED` | The menu and protected three-step route offer Port, Web, Script, and Aggregate only; Metric and RAW are absent. | Verify menu flag and direct-route permission independently. |
+| `ALERT-CREATE-002` | `IMPROVED_BEYOND_CLASSIC` | Common and type-specific validation produce Server-compatible source payloads; Port/Web match Classic while Script/Aggregate deliberately repair broken Classic paths. | Create and execute every type against Agent/Server. |
+| `ALERT-CREATE-003` | `STATICALLY_ALIGNED` | Step 3 renders the exact immutable request payload and Back returns to the populated editor. | Confirm browser layout and payload review for every type. |
+| `ALERT-CREATE-004` | `STATICALLY_ALIGNED` | Submission uses a synchronous in-flight guard, POSTs the exact collection once, retains the wizard on failure, and leaves only after success. | Fault-inject the POST and double-click in a real browser. |
+| `ALERT-CREATE-005` | `STATICALLY_ALIGNED` | `/main/alerts/add/:stepNumber` supports Back and validation-gated forward entry; direct Step 2/3 entry is redirected to the earliest required step. | Exercise reload and manual URL entry at every step. |
+
+### Alert Groups
+
+| ID | Static status | React implementation evidence | Remaining live evidence |
+| --- | --- | --- | --- |
+| `ALERT-GROUP-001` | `STATICALLY_ALIGNED` | Groups and notifications load independently with visible Retry states. | Verify large-group rendering and Server failures. |
+| `ALERT-GROUP-002` | `STATICALLY_ALIGNED` | Classic-compatible trimmed name validation and case-insensitive uniqueness checks run before local creation; failed creates remain pending. | Confirm concurrent-name rejection handling. |
+| `ALERT-GROUP-003` | `STATICALLY_ALIGNED` | Rename and copy use the complete local group model and preserve definition/target IDs. | Round-trip renamed and copied groups. |
+| `ALERT-GROUP-004` | `STATICALLY_ALIGNED` | Definition changes remain local, default-group restrictions remain enforced, and Save sends numeric replacement arrays. | Exercise service/component filters and default groups. |
+| `ALERT-GROUP-005` | `STATICALLY_ALIGNED` | Target assignments remain local and are sent in `AlertGroup.targets`; unsupported pseudo membership resources were removed. | Reload assignments from Server. |
+| `ALERT-GROUP-006` | `STATICALLY_ALIGNED` | Default deletion is blocked; failed deletes remain pending in the open editor. | Inject referenced/concurrent delete failures. |
+| `ALERT-GROUP-007` | `STATICALLY_ALIGNED` | All deletes settle before concurrent updates/creates; every result is aggregated, and refreshed Server state reconciles ambiguous timeout outcomes by ID and complete membership payload. | Fault-inject partial success, timeout-after-commit, and refresh failure. |
+
+### Alert Notifications
+
+| ID | Static status | React implementation evidence | Remaining live evidence |
+| --- | --- | --- | --- |
+| `ALERT-NOTIFY-001` | `STATICALLY_ALIGNED` | The permission-gated modal renders target scope, severity, enabled state, API type, and description with load Retry. | Validate real target variants and roles. |
+| `ALERT-NOTIFY-002` | `STATICALLY_ALIGNED` | Email validation and payload construction cover recipients, SMTP host/port/from, optional authentication, TLS, and credentials. | Validate dispatcher configuration against Server. |
+| `ALERT-NOTIFY-003` | `STATICALLY_ALIGNED` | Built-in SNMP maps to `AMBARI_SNMP` and uses `ambari.dispatch.recipients`. | Round-trip SNMP v1/v2c. |
+| `ALERT-NOTIFY-004` | `STATICALLY_ALIGNED` | Custom SNMP maps to `SNMP` and writes trap/subject/body OID properties. | Round-trip a Custom SNMP target. |
+| `ALERT-NOTIFY-005` | `STATICALLY_ALIGNED` | Alert Script maps to `ALERT_SCRIPT` and the two Classic dispatch property names. | Execute a deployed notification script. |
+| `ALERT-NOTIFY-006` | `STATICALLY_ALIGNED` | Global disables group assignment; custom scope and severity arrays are submitted exactly. | Verify group association reload. |
+| `ALERT-NOTIFY-007` | `STATICALLY_ALIGNED` | Edit preserves an unchanged returned sensitive password or replaces it explicitly; copies require a unique name and do not silently reuse a hidden credential. | Exercise masked-property semantics against Server. |
+| `ALERT-NOTIFY-008` | `STATICALLY_ALIGNED` | Enable/disable sends only `AlertTarget/is_enabled`, mutates selection only after HTTP success, and reloads targets. | Inject toggle and reload failures. |
+| `ALERT-NOTIFY-009` | `STATICALLY_ALIGNED` | Delete failure remains visible in the confirmation modal with the target selected; success closes and refreshes. | Exercise a target referenced by groups. |
+| `ALERT-NOTIFY-010` | `STATICALLY_ALIGNED` | Custom keys validate syntax, uniqueness, and conflicts against exact type-specific built-ins. | Validate uncommon dispatcher properties. |
+
+### Global Alert Settings
+
+| ID | Static status | React implementation evidence | Remaining live evidence |
+| --- | --- | --- | --- |
+| `ALERT-SET-001` | `STATICALLY_ALIGNED` | Detail repeat tolerance shares validated 1-99/`DEBUG` semantics and retains errors for Retry. | Round-trip values against Server. |
+| `ALERT-SET-002` | `STATICALLY_ALIGNED` | Global settings preserve `DEBUG`, await the `cluster-env` save/reload, and keep the dialog open on failure. | Verify a new desired-config version and reload ordering. |
+| `ALERT-SET-003` | `LIVE_REQUIRED` | React displays maintenance counts and states; mutations remain owned by Host, Service, and Admin modules. Static Alerts code cannot establish notification suppression convergence. | Run the cross-module maintenance matrix. |
+
+## Reverse API Contract
+
+| Flow | Method and resource | Required query or payload |
+| --- | --- | --- |
+| List definitions | `GET /api/v1/clusters/{cluster}/alert_definitions` | Definition identity, display, enabled, source, interval, tolerance, groups, and non-Metric configuration fields needed by list/details. |
+| Definition by ID | `GET /api/v1/clusters/{cluster}/alert_definitions/{id}` | `fields=*`; details must not depend on membership in a loaded group. |
+| Grouped summary | `GET /api/v1/clusters/{cluster}/alerts?format=groupedSummary` | Merge by definition ID and retain status counts and timestamps. |
+| Definition instances | `GET /api/v1/clusters/{cluster}/alerts?fields=*&Alert/definition_id={id}` | Poll only while details are mounted. |
+| 24-hour history | `GET /api/v1/clusters/{cluster}/alert_history?(AlertHistory/definition_name={name})&(AlertHistory/timestamp>={now-86400000})` | Count returned `AlertHistory.host_name` records per host. |
+| Update definition | `PUT /api/v1/clusters/{cluster}/alert_definitions/{id}` | Slash-keyed properties such as `AlertDefinition/label`, `AlertDefinition/enabled`, and validated config/source fields. |
+| Create definition | `POST /api/v1/clusters/{cluster}/alert_definitions/` | Exact reviewed payload containing common definition fields and the selected non-Metric source. |
+| Load groups | `GET /api/v1/clusters/{cluster}/alert_groups?fields=*` | Preserve definition and target references. |
+| Create group | `POST /api/v1/clusters/{cluster}/alert_groups` | `{ "AlertGroup": { "name": string, "definitions": number[], "targets": number[] } }`. Empty arrays remain explicit replacement values. |
+| Update group | `PUT /api/v1/clusters/{cluster}/alert_groups/{id}` | Same complete `AlertGroup` replacement payload as create. |
+| Delete group | `DELETE /api/v1/clusters/{cluster}/alert_groups/{id}` | Only non-default groups; delete phase completes before update/create phase begins. |
+| Load targets | `GET /api/v1/alert_targets?fields=*` | Preserve server type, scope, groups, states, enabled, and property values including masked sensitive values. |
+| Create target | `POST /api/v1/alert_targets` | `{ "AlertTarget": ... }` with API type and exact type-specific properties. |
+| Update target | `PUT /api/v1/alert_targets/{id}` | Complete edited target or the intentional enabled-only partial update. |
+| Delete target | `DELETE /api/v1/alert_targets/{id}` | Retain UI state when the server rejects a referenced target. |
+| Global repeat tolerance | `PUT /api/v1/clusters/{cluster}` through the existing config saver | New `cluster-env` version preserving `alerts_repeat_tolerance` as `DEBUG` or a decimal string. |
+
+The React pseudo-resources `/alert_groups/{group}/alert_definitions/{definition}` and `/alert_groups/{group}/alert_targets/{target}` do not appear in the inspected Classic contract. Group membership and target association are replacement arrays on Alert Group create/update.
+
+## Notification Property Contract
+
+| UI type | API `notification_type` | Required built-in properties |
+| --- | --- | --- |
+| Email | `EMAIL` | `ambari.dispatch.recipients`, `mail.smtp.host`, `mail.smtp.port`, `mail.smtp.from`, `mail.smtp.auth`; when authentication is enabled, credential username/password and `mail.smtp.starttls.enable` |
+| SNMP | `AMBARI_SNMP` | `ambari.dispatch.snmp.version`, `ambari.dispatch.snmp.community`, `ambari.dispatch.recipients`, `ambari.dispatch.snmp.port` |
+| Custom SNMP | `SNMP` | SNMP version/community/recipients/port plus `ambari.dispatch.snmp.oids.trap`, `.oids.subject`, and `.oids.body` |
+| Alert Script | `ALERT_SCRIPT` | Optional `ambari.dispatch-property.script` and `ambari.dispatch-property.script.filename` |
+
+Custom properties cannot duplicate any built-in key for the selected type. An unchanged masked Email password must retain the server's sensitive-property semantics instead of being replaced with an empty string.
+
+## Permissions, Feature Flags, and Routes
+
+| Entry or operation | Required Classic gate | Current React state |
+| --- | --- | --- |
+| Alerts list | Normal authenticated cluster route | Present. |
+| List enable/disable | `CLUSTER.TOGGLE_ALERTS` | Aligned. |
+| Detail edit and enable/disable | `SERVICE.TOGGLE_ALERTS` | Aligned. |
+| Create menu entry | `supports.createAlerts` | Aligned. |
+| Direct create wizard route | `SERVICE.TOGGLE_ALERTS` | Aligned independently of menu visibility. |
+| Manage Groups | No independent permission gate | Aligned; the action remains visible. |
+| Manage Notifications | `CLUSTER.MANAGE_ALERT_NOTIFICATIONS` | Aligned. |
+| Manage Settings | No independent permission gate | Aligned; the action remains visible. |
+
+The target create route is `/main/alerts/add/:stepNumber`. It must use `ProtectedRoute requireAuthorization="SERVICE.TOGGLE_ALERTS"`; menu visibility additionally checks `supports.createAlerts`, matching the distinction in Classic.
+
+## Pre-Implementation Five-Pass Audit Record
+
+| Pass | Evidence inspected | Main conclusions |
+| --- | --- | --- |
+| 1. User-visible surface | Classic routes, templates, views, menus; React routes and Alerts components | Create is absent; deletion is dead code; group description is not a Classic feature; Actions visibility is too restrictive. |
+| 2. State and controller logic | Classic definition, group, notification, config, and wizard controllers; React hooks and modal state | Dirty navigation is absent; group associations mutate too early; failures commonly discard or hide state. |
+| 3. API contract | Generated endpoints/call sites and concrete request builders in both implementations | History is missing; group targets are stripped; notification type/property mappings are wrong; pseudo membership resources are unsupported by Classic evidence. |
+| 4. Permissions and flags | Generated permissions/flags, Classic authorization helpers/routes, React guards/hooks/context | List toggle uses the wrong permission; Create needs separate flag and route authorization; Groups/Settings must remain visible. |
+| 5. Failure, concurrency, and recovery | Classic callbacks/save sequencing and React async paths | Group phase ordering/all-settled behavior differs; list/details/modals lack consistent Retry; sensitive Email edit and response text opening are unsafe. |
+
+## Live Runtime Acceptance Matrix
+
+| Scenario | Required live evidence |
+| --- | --- |
+| Definition list refresh | Status counts, timestamps, current filters, sorting, and selected page remain coherent through polling and STOMP or server-side changes. |
+| List permission boundary | A user with only cluster toggle can toggle from the list; a service-toggle-only user cannot; denied direct calls do not leave stale optimistic state. |
+| Action visibility | Users without alert mutation permissions still see Groups and Settings; Notifications and Create follow their independent gates. |
+| Direct definition load | A valid definition with no non-default group membership opens directly by ID and renders complete non-Metric information. |
+| Dirty route leave | Label-only, config-only, and combined edits exercise Save, Discard, Cancel, browser Back, in-app Back, and refresh/close protection. |
+| Definition save failure | Validation prevents bad input; transport/server failures keep values and allow a successful Retry. |
+| Instance polling | Polling starts on entry, stops on exit, survives one failed request, and correctly links host/service rows. |
+| Response text safety | Response strings containing markup and script-like text are copied/opened literally and never interpreted. |
+| 24-hour history | Multiple records on multiple hosts produce exact per-host counts at the timestamp boundary. |
+| Create each type | Port and Web create exact definitions; repaired Script and Aggregate paths are identified as beyond-Classic behavior; Metric and RAW are absent. |
+| Create authorization and flag | Menu flag and direct-route permission combinations are tested independently, including URL entry. |
+| Group successful save | Concurrent deletes finish before concurrent updates/creates; definitions and targets persist after reload. |
+| Group partial failure | Every started request settles, aggregate failures are shown, and unsaved state remains available for reconciliation and Retry. |
+| Notification types | Email, built-in SNMP, Custom SNMP, and Alert Script round-trip with exact API types and properties. |
+| Sensitive Email edit | Saving an authenticated Email without entering a new password does not erase or replace the stored credential. |
+| Notification failure | Create, edit, enable/disable, and delete failures retain the selected target and permit Retry. |
+| Repeat tolerance | Values 1, 99, and `DEBUG` round-trip; invalid values are rejected; save failure leaves the dialog open. |
+| Maintenance interaction | Host/service maintenance and alert toggles update presentation and notification suppression after owner-module operations converge. |
+
+## Focused Executable Evidence
+
+Module 05 now includes deterministic tests for:
+
+- exact Alert API URLs, methods, query values, and payloads;
+- create-form validation and request generation for every supported non-Metric type;
+- menu feature/permission policy and direct route protection;
+- history count mapping by host;
+- definition config validation, description preservation, and dirty-state decisions;
+- safe response opening without HTML interpretation;
+- complete group payloads and delete-before-update/create all-settled orchestration;
+- notification UI/API type conversion, property construction, built-in conflicts, and sensitive Email edits;
+- `DEBUG` repeat-tolerance preservation; and
+- rejected load/toggle/save operations retaining state for Retry.
+
+The focused evidence is in `src/api/alertsApi.test.ts`, `src/Utils/alertCreation.test.ts`, `src/Utils/alertDefinitions.test.ts`, `src/Utils/alertGroups.test.ts`, `src/Utils/alertNotifications.test.ts`, `src/Utils/alertPolicy.test.ts`, `src/screens/Alerts/alertUtils.test.ts`, and `src/screens/Alerts/AlertInstancesTable.test.tsx`.
+
+Runtime-required rows must remain in this document even after focused tests pass. They are removed only after the corresponding behavior has been accepted against a live Ambari cluster.
diff --git a/docs/frontend-refactor/react-current/issues/05-alerts.md b/docs/frontend-refactor/react-current/issues/05-alerts.md
new file mode 100644
index 0000000000..75becd55a9
--- /dev/null
+++ b/docs/frontend-refactor/react-current/issues/05-alerts.md
@@ -0,0 +1,98 @@
+<!---
+   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.
+-->
+
+## Problem
+
+The React Alerts module provides a substantial list, details, Alert Group, and Alert Notification UI, but several high-risk behaviors do not match the classic Ambari contract. Alert Definition creation and 24-hour instance history are missing. List and action permissions are incorrect, definition/config editing lacks validation and route-leave protection, and common load or save failures are not recoverable.
+
+Alert Group requests omit notification target IDs and use unsupported pseudo membership resources. Notification builders send incorrect API types and property names for built-in SNMP, Custom SNMP, and Alert Script targets, and authenticated Email edits can overwrite an unchanged sensitive password. Instance response text can also be interpreted as markup when opened in a new window.
+
+This issue covers the complete non-Metrics Alerts module defined by the corrected Classic baseline. Metric Alert Definition parameters, metric expressions, metric data, and Metrics APIs are excluded.
+
+## Scope
+
+* Load definitions directly by ID and expose recoverable list, details, instance, group, and notification failures.
+* Use `CLUSTER.TOGGLE_ALERTS` for list toggles and `SERVICE.TOGGLE_ALERTS` for definition detail edits, toggles, and the direct create route.
+* Keep Manage Groups and Manage Settings visible without an independent permission gate, gate Notifications with `CLUSTER.MANAGE_ALERT_NOTIFICATIONS`, and gate the Create menu with `supports.createAlerts`.
+* Add the three-step `/main/alerts/add/:stepNumber` wizard for Port, Web, Script, and Aggregate definitions, with validation, read-only review, exact request construction, recoverable submission, and back/forward navigation.
+* Keep Metric and RAW creation out of the wizard. Document the working Script and Aggregate paths as intentional repairs beyond broken Classic behavior.
+* Query `/alert_history` for the previous 24 hours and map record counts by host instead of using the current instance update timestamp.
+* Validate labels, interval/timeout values, parameters, thresholds, repeat tolerance, and `DEBUG`; preserve description and source/config values in update payloads.
+* Coordinate dirty label/config edits with Save, Discard, and Cancel behavior and retain edit state after failed saves.
+* Open and copy Alert Instance response text literally without HTML interpretation.
+* Build complete Alert Group create/update replacement payloads containing name, definition IDs, and notification target IDs.
+* Keep group definition and target changes local until Save; run all deletes first, then updates and creates, wait for every result, aggregate failures, and keep the original editor open after partial failure.
+* Map Email, SNMP, Custom SNMP, and Alert Script UI models to their exact server types and built-in properties.
+* Preserve an unchanged sensitive Email password on edit, support explicit replacement, and reject custom property names that conflict with built-in keys.
+* Await global repeat-tolerance configuration saves, preserve `DEBUG`, and retain the dialog after failure.
+* Remove Alerts debugging output from touched code and add focused tests for request shapes, state transitions, validation, permissions, concurrency, and failure recovery.
+
+## Classic UI Baseline
+
+The acceptance baseline is `docs/frontend-refactor/ember-baseline/05-alerts.md`, feature IDs `ALERT-LIST-001` through `ALERT-SET-003`. The detailed React comparison, reverse API contract, permission matrix, five-pass audit, compatibility decisions, and live runtime matrix are recorded in `docs/frontend-refactor/react-current/05-alerts-gap.md`.
+
+The baseline was corrected during the audit:
+
+* Classic contains an Alert Definition delete controller method and AJAX definition, but no inspected UI call site. It is unreachable dead code, not a user-facing workflow.
+* Port and Web creation have functional-looking paths. RAW has no renderer; Script dereferences missing wizard data; Aggregate omits common fields and dereferences a missing definition name.
+* Classic Alert Groups contain a name, definitions, and notification targets but no description field.
+
+The authoritative network comparison includes global AJAX definitions and call sites, direct HTTP and browser entry points, routes, permissions, feature flags, and realtime destinations. Group membership is represented by replacement arrays on Alert Group create/update, not by child membership resources.
+
+## Acceptance Criteria
+
+* Alert list data remains coherent across polling and navigation, displays load failure and Retry, and applies filters and sorting without losing state.
+* List toggles require `CLUSTER.TOGGLE_ALERTS`; details and direct creation require `SERVICE.TOGGLE_ALERTS`; Create and Notifications obey their independent feature and permission gates.
+* Groups and Settings remain reachable by users without toggle or notification permissions.
+* Details load the exact definition by ID, including definitions without non-default group membership.
+* Label and non-Metric configuration edits validate input, preserve description/source fields, await saves, keep failed edits retryable, and protect dirty navigation with Save, Discard, and Cancel.
+* Alert Instance polling stops on exit, survives a failed request, and displays exact 24-hour history counts per host.
+* Response text containing markup or script-like content is copied and opened as literal text.
+* Port, Web, Script, and Aggregate creation validates each step, shows the exact payload at review, submits once, and retains the wizard after failure; Metric and RAW types are not offered.
+* Alert Group create/update payloads contain complete numeric `definitions` and `targets` arrays and never clear associations during rename or copy.
+* Group Save waits for concurrent deletes before concurrent updates/creates, waits for every started request, reports aggregate errors, and preserves pending state after any failure.
+* Email maps to `EMAIL`, built-in SNMP to `AMBARI_SNMP`, Custom SNMP to `SNMP`, and Alert Script to `ALERT_SCRIPT` with exact Classic property names.
+* An authenticated Email edit without a replacement password retains the existing sensitive property; custom properties cannot conflict with corrected built-in properties.
+* Notification create, edit, enable/disable, and delete failures retain selection and expose Retry.
+* Per-definition and global repeat tolerance accepts 1 through 99 or `DEBUG`, preserves the sentinel, and does not close before a successful save.
+* Focused tests cover exact URLs, methods, queries, payloads, creation, history mapping, validation, dirty decisions, permission policy, safe response handling, group sequencing, notification conversion, sensitive edits, and rejected operations.
+* The applicable runtime matrix in `docs/frontend-refactor/react-current/05-alerts-gap.md` passes against a real Ambari Server.
+
+## Compatibility Decisions
+
+The React implementation must not reproduce known broken or unsafe Classic behavior:
+
+* It does not add Alert Definition deletion UI solely because an unreachable Classic controller method exists.
+* It repairs Script and Aggregate creation rather than reproducing their broken Classic wizard data paths; this is identified as `IMPROVED_BEYOND_CLASSIC`.
+* It does not add an Alert Group description field that is absent from the inspected Classic UI and request contract.
+* It waits for mutation results and preserves retryable state instead of reproducing Classic's optimistic close or incomplete rollback behavior.
+* It opens server-provided response data through text nodes rather than `document.write()`.
+* It preserves unchanged sensitive values instead of submitting empty credentials.
+
+These are intentional compatibility corrections, not missing parity.
+
+## Out of Scope
+
+* Metric Alert Definition parameters, metric expressions, metric data, Metrics APIs, Metrics routes, charts, and widgets.
+* RAW creation, because the Classic type is an unimplemented placeholder.
+* New Alert Definition deletion UI, because the Classic method is unreachable dead code.
+* Alert Group descriptions, because the inspected Classic contract has no such field.
+* Host, Service, upgrade, installation, Kerberos, and HA mutation implementations owned by other modules; this issue validates only their documented Alerts presentation boundary.
+
+## Verification Boundary
+
+Static code and focused tests are not sufficient to complete runtime acceptance. The live matrix must still cover polling and realtime convergence, permission-role combinations, direct URL entry, dirty browser navigation, real Server validation, group partial failures, notification round trips, masked sensitive properties, maintenance interactions, and every supported non-Metric creation type.


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ambari.apache.org
For additional commands, e-mail: commits-help@ambari.apache.org