Repository: keephq/keep
Pull request: feat: incidents

Problem statement:
[➕ Feature]: Incidents
Introduce the incident page and the Incident data model.

**Data model:**
For now, incidents from some providers are pulled as "alerts", they should be pulled as "incidents".

**UI:**
User should be able to manually CRUD Many:One relationships between Alerts and Incidents. Also please note that most probably user would love to add multiple alerts to the incident in a bulk way.

Hints from issue discussion:
@Matvey-Kuk @VladimirFilonov can we maybe elaborate further on the actual data model for incidents (e.g., actual fields: incident id, name, description, started, ended, who participated, affected services, etc.)

@Matvey-Kuk is it true that alerts can be related with more than 1 incident? e.g. it's Many<alerts>:One<incident> but same alert can be referred to more than 1 incident?
@talboren re data model, I propose to keep-it-simple for the beginning:
- id
- name
- description
- service(s)
- assignee(s)
- no start time & no end time, we may calculate them through the first and the last alert

> it true that alerts can be related with more than 1 incident

I don't know a real-life example where it could be true, but it may complicate UI perception a lot, so I'm a bit hesitant to this idea.
> I don't know a real-life example where it could be true, but it may complicate UI perception a lot, so I'm a bit hesitant to this idea.

In what sense? I can't share a real-life example too but I can see why it makes sense that 2 non-independent incidents (in different parts of the system though) might share the same alerts as context for RCA. I'm just worried about narrowing down our options here (and I can't see the complications)
Good example, let's imagine we have an `alert_a` being a part of `incident_b` and `incident_c`.

We have options: 
1. Connect these alert to both incidents.
2. Wrap `alert_a` to a `incident_d` and link it to `incident_b` and `incident_c` as a "root cause". 

I'm leaning towards 2 because:
1. We'll need to introduce root cause graph for dependent incidents anyway.
2. We may want to bring per-incident bulk actions for alerts. If alerts will belong to multiple incidents, it may confuse users.

Not strong opinion anyways, also I think we may migrate the data scheme from simple to complicated any moment.

CC @GlebBerjoskin 
> Good example, let's imagine we have an `alert_a` being a part of `incident_b` and `incident_c`.
> 
> We have options:
> 
> 1. Connect these alert to both incidents.
> 2. Wrap `alert_a` to a `incident_d` and link it to `incident_b` and `incident_c` as a "root cause".
> 
> I'm leaning towards 2 because:
> 
> 1. We'll need to introduce root cause graph for dependent incidents anyway.
> 2. We may want to bring per-incident bulk actions for alerts. If alerts will belong to multiple incidents, it may confuse users.
> 
> Not strong opinion anyways, also I think we may migrate the data scheme from simple to complicated any moment.
> 
> CC @GlebBerjoskin

No strong opinion too but it was a little hard for me to understand reading quickly, so I lean towards over-engineering in those cases :P
I feel like a reference table of `AlertId` to `IncidentId` is good enough as a beginning and then adjusting might be "easier"

Commit message: fix: tests

Patch to review:
diff --git a/LICENSE b/LICENSE
index 442fd8f34f..ff7b6827b6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,10 @@
-MIT License
-
 Copyright (c) 2024 Keep
 
+Portions of this software are licensed as follows:
+
+* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
+* Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined below.
+
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
diff --git a/docker/Dockerfile.dev.api b/docker/Dockerfile.dev.api
index 2544288462..5b3f12b703 100644
--- a/docker/Dockerfile.dev.api
+++ b/docker/Dockerfile.dev.api
@@ -23,4 +23,4 @@ ENV PATH="/venv/bin:${PATH}"
 ENV VIRTUAL_ENV="/venv"
 
 
-ENTRYPOINT ["gunicorn", "keep.api.api:get_app", "--bind" , "0.0.0.0:8080" , "--workers", "1" , "-k" , "uvicorn.workers.UvicornWorker", "-c", "./keep/api/config.py", "--reload"]
+CMD ["gunicorn", "keep.api.api:get_app", "--bind" , "0.0.0.0:8080" , "--workers", "1" , "-k" , "uvicorn.workers.UvicornWorker", "-c", "./keep/api/config.py", "--reload"]
diff --git a/ee/LICENSE b/ee/LICENSE
new file mode 100644
index 0000000000..3395cf32da
--- /dev/null
+++ b/ee/LICENSE
@@ -0,0 +1,35 @@
+The Keep Enterprise Edition (EE) license (the Enterprise License)
+Copyright (c) 2024-present Keep Alerting LTD
+
+With regard to the Keep Software:
+
+This software and associated documentation files (the "Software") may only be
+used in production, if you (and any entity that you represent) have agreed to,
+and are in compliance with, the Keep Subscription Terms of Service, available 
+(if not available, it's impossible to comply)
+at https://www.keephq.dev/terms-of-service (the "The Enterprise Terms”), or other
+agreement governing the use of the Software, as agreed by you and Keep,
+and otherwise have a valid Keep Enterprise Edition subscription for the
+correct number of user seats. Subject to the foregoing sentence, you are free to
+modify this Software and publish patches to the Software. You agree that Keep
+and/or its licensors (as applicable) retain all right, title and interest in and
+to all such modifications and/or patches, and all such modifications and/or
+patches may only be used, copied, modified, displayed, distributed, or otherwise
+exploited with a valid Keep Enterprise Edition subscription for the  correct
+number of user seats. You agree that Keep and/or its licensors (as applicable) retain
+all right, title and interest in and to all such modifications.  You are not
+granted any other rights beyond what is expressly stated herein. Subject to the
+foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
+and/or sell the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+For all third party components incorporated into the Keep Software, those
+components are licensed under the original license provided by the owner of the
+applicable component.
\ No newline at end of file
diff --git a/ee/experimental/__init__.py b/ee/experimental/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ee/experimental/incident_utils.py b/ee/experimental/incident_utils.py
new file mode 100644
index 0000000000..975e64ff2d
--- /dev/null
+++ b/ee/experimental/incident_utils.py
@@ -0,0 +1,148 @@
+import numpy as np
+import pandas as pd
+import networkx as nx
+
+from typing import List
+
+from keep.api.models.db.alert import Alert
+
+
+def mine_incidents(alerts: List[Alert], incident_sliding_window_size: int=6*24*60*60, statistic_sliding_window_size: int=60*60, 
+                   jaccard_threshold: float=0.0, fingerprint_threshold: int=1):
+    """
+        Mine incidents from alerts.
+    """
+    
+    alert_dict = {
+        'fingerprint': [alert.fingerprint for alert in alerts],
+        'timestamp': [alert.timestamp for alert in alerts],
+    }
+    alert_df = pd.DataFrame(alert_dict)
+    mined_incidents = shape_incidents(alert_df, 'fingerprint', incident_sliding_window_size, statistic_sliding_window_size, 
+                                      jaccard_threshold, fingerprint_threshold)
+    
+    return [
+        {
+            "incident_fingerprint": incident['incident_fingerprint'],
+            "alerts": [alert for alert in alerts if alert.fingerprint in incident['alert_fingerprints']],
+        }
+        for incident in mined_incidents
+    ]
+
+
+def get_batched_alert_counts(alerts: pd.DataFrame, unique_alert_identifier: str, sliding_window_size: int) -> np.ndarray:
+    """
+        Get the number of alerts in a sliding window.
+    """
+
+    resampled_alert_counts = alerts.set_index('timestamp').resample(
+        f'{sliding_window_size//2}s')[unique_alert_identifier].value_counts().unstack(fill_value=0)
+    rolling_counts = resampled_alert_counts.rolling(
+        window=f'{sliding_window_size}s', min_periods=1).sum()
+    alert_counts = rolling_counts.to_numpy()
+
+    return alert_counts
+
+
+def get_batched_alert_occurrences(alerts: pd.DataFrame, unique_alert_identifier: str, sliding_window_size: int) -> np.ndarray:
+    """
+        Get the occurrence of alerts in a sliding window.
+    """
+
+    alert_counts = get_batched_alert_counts(
+        alerts, unique_alert_identifier, sliding_window_size)
+    alert_occurences = np.where(alert_counts > 0, 1, 0)
+
+    return alert_occurences
+
+
+def get_jaccard_scores(P_a: np.ndarray, P_aa: np.ndarray) -> np.ndarray:
+    """
+        Calculate the Jaccard similarity scores between alerts.
+    """
+
+    P_a_matrix = P_a[:, None] + P_a
+    union_matrix = P_a_matrix - P_aa
+
+    with np.errstate(divide='ignore', invalid='ignore'):
+        jaccard_matrix = np.where(union_matrix != 0, P_aa / union_matrix, 0)
+
+    np.fill_diagonal(jaccard_matrix, 1)
+
+    return jaccard_matrix
+
+
+def get_alert_jaccard_matrix(alerts: pd.DataFrame, unique_alert_identifier: str, sliding_window_size: int) -> np.ndarray:
+    """
+        Calculate the Jaccard similarity scores between alerts.
+    """
+
+    alert_occurrences = get_batched_alert_occurrences(
+        alerts, unique_alert_identifier, sliding_window_size)
+    alert_probabilities = np.mean(alert_occurrences, axis=0)
+    joint_alert_occurrences = np.dot(alert_occurrences.T, alert_occurrences)
+    pairwise_alert_probabilities = joint_alert_occurrences / \
+        alert_occurrences.shape[0]
+
+    return get_jaccard_scores(alert_probabilities, pairwise_alert_probabilities)
+
+
+def build_graph_from_occurrence(occurrence_row: pd.DataFrame, jaccard_matrix: np.ndarray, unique_alert_identifiers: List[str], 
+                                jaccard_threshold: float = 0.05) -> nx.Graph:
+    """
+        Build a weighted graph using alert occurrence matrix and Jaccard coefficients.
+    """
+
+    present_indices = np.where(occurrence_row > 0)[0]
+
+    G = nx.Graph()
+
+    for idx in present_indices:
+        alert_desc = unique_alert_identifiers[idx]
+        G.add_node(alert_desc)
+
+    for i in present_indices:
+        for j in present_indices:
+            if i != j and jaccard_matrix[i, j] >= jaccard_threshold:
+                alert_i = unique_alert_identifiers[i]
+                alert_j = unique_alert_identifiers[j]
+                G.add_edge(alert_i, alert_j, weight=jaccard_matrix[i, j])
+
+    return G
+
+def shape_incidents(alerts: pd.DataFrame, unique_alert_identifier: str, incident_sliding_window_size: int, statistic_sliding_window_size: int, 
+                   jaccard_threshold: float = 0.2, fingerprint_threshold: int = 5) -> List[dict]:
+    """
+        Shape incidents from alerts.
+    """
+    
+    incidents = []
+    incident_number = 0
+
+    resampled_alert_counts = alerts.set_index('timestamp').resample(
+        f'{incident_sliding_window_size//2}s')[unique_alert_identifier].value_counts().unstack(fill_value=0)
+    jaccard_matrix = get_alert_jaccard_matrix(
+        alerts, unique_alert_identifier, statistic_sliding_window_size)
+
+    for idx in range(resampled_alert_counts.shape[0]):
+        graph = build_graph_from_occurrence(
+            resampled_alert_counts.iloc[idx], jaccard_matrix, resampled_alert_counts.columns, jaccard_threshold=jaccard_threshold)
+        max_component = max(nx.connected_components(graph), key=len)
+
+        min_starts_at = resampled_alert_counts.index[idx]
+        max_starts_at = min_starts_at + \
+            pd.Timedelta(seconds=incident_sliding_window_size)
+
+        local_alerts = alerts[(alerts['timestamp'] >= min_starts_at) & (
+            alerts['timestamp'] <= max_starts_at)]
+        local_alerts = local_alerts[local_alerts[unique_alert_identifier].isin(
+            max_component)]
+
+        if len(max_component) > fingerprint_threshold:
+
+            incidents.append({
+                'incident_fingerprint': f'Incident #{incident_number}',
+                'alert_fingerprints': local_alerts[unique_alert_identifier].unique().tolist(),
+            })
+
+    return incidents
\ No newline at end of file
diff --git a/keep-ui/app/ai/ai.tsx b/keep-ui/app/ai/ai.tsx
new file mode 100644
index 0000000000..d41284ae60
--- /dev/null
+++ b/keep-ui/app/ai/ai.tsx
@@ -0,0 +1,152 @@
+"use client";
+import { Card, List, ListItem, Title, Subtitle } from "@tremor/react";
+import { useAIStats } from "utils/hooks/useAIStats";
+import { useSession } from "next-auth/react";
+import { getApiURL } from "utils/apiUrl";
+import { toast } from "react-toastify";
+import { useEffect, useState, useRef, FormEvent } from "react";
+
+export default function Ai() {
+  const { data: aistats, isLoading } = useAIStats();
+  const { data: session } = useSession();
+  const [text, setText] = useState("");
+  const [newText, setNewText] = useState("Mine incidents");
+  const [animate, setAnimate] = useState(false);
+  const onlyOnce = useRef(false);
+
+  useEffect(() => {
+    let index = 0;
+
+    const interval = setInterval(() => {
+      setText(newText.slice(0, index + 1));
+      index++;
+
+      if (index === newText.length) {
+        clearInterval(interval);
+      }
+    }, 100);
+
+    return () => {
+      clearInterval(interval);
+    };
+  }, [newText]);
+
+  const mineIncidents = async (e: FormEvent) => {
+    e.preventDefault();
+    setAnimate(true);
+    setNewText("Mining 🚀🚀🚀 ...");
+    const apiUrl = getApiURL();
+    const response = await fetch(`${apiUrl}/incidents/mine`, {
+      method: "POST",
+      headers: {
+        Authorization: `Bearer ${session?.accessToken}`,
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+      }),
+    });
+    if (response.ok) {
+      setAnimate(false);
+      setNewText("Mine incidents");
+    } else {
+      setAnimate(false);
+      setNewText("Mine incidents");
+      toast.error(
+        "Failed to mine incidents, please contact us if this issue persists."
+      );
+    }
+  };
+
+  return (
+    <main className="p-4 md:p-10 mx-auto max-w-full">
+      <div className="flex justify-between items-center">
+        <div>
+          <Title>AI Correlation</Title>
+          <Subtitle>
+            Correlating alerts to incidents based on past alerts, incidents, and
+            the other data.
+          </Subtitle>
+        </div>
+      </div>
+      <Card className="mt-10 p-4 md:p-10 mx-auto">
+        <div>
+          <div className="prose-2xl">👋 You are almost there!</div>
+          AI Correlation is coming soon. Make sure you have enough data collected to prepare.
+          <div className="max-w-md mt-10 flex justify-items-start justify-start">
+            <List>
+              <ListItem>
+                <span>
+                  Connect an incident source to dump incidents, or create 10
+                  incidents manually
+                </span>
+                <span>
+                  {aistats?.incidents_count &&
+                  aistats?.incidents_count >= 10 ? (
+                    <div>✅</div>
+                  ) : (
+                    <div>⏳</div>
+                  )}
+                </span>
+              </ListItem>
+              <ListItem>
+                <span>Collect 100 alerts</span>
+                <span>
+                  {aistats?.alerts_count && aistats?.alerts_count >= 100 ? (
+                    <div>✅</div>
+                  ) : (
+                    <div>⏳</div>
+                  )}
+                </span>
+              </ListItem>
+              <ListItem>
+                <span>Collect alerts for more than 3 days</span>
+                <span>
+                  {aistats?.first_alert_datetime && new Date(aistats.first_alert_datetime) < new Date(Date.now() - 3 * 24 * 60 * 60 * 1000) ? (
+                    <div>✅</div>
+                  ) : (
+                    <div>⏳</div>
+                  )}
+                </span>
+              </ListItem>
+            </List>
+          </div>
+          {(aistats?.is_mining_enabled && <button
+            className={
+              (animate && "animate-pulse") +
+              " w-full text-white mt-10 pt-2 pb-2 pr-2 rounded-xl transition-all duration-500 bg-gradient-to-tl from-amber-800 via-amber-600 to-amber-400 bg-size-200 bg-pos-0 hover:bg-pos-100"
+            }
+            onClick={ mineIncidents }
+          ><div className="flex flex-row p-2">
+            <div className="p-2">
+              {animate && <svg
+                  className="animate-spin h-6 w-6 text-white"
+                  xmlns="http://www.w3.org/2000/svg"
+                  fill="none"
+                  viewBox="0 0 24 24"
+                >
+                  <circle
+                    className="opacity-25"
+                    cx="12"
+                    cy="12"
+                    r="10"
+                    stroke="currentColor"
+                    stroke-width="4"
+                  ></circle>
+                  <path
+                    className="opacity-75"
+                    fill="currentColor"
+                    d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+                  ></path>
+                </svg>}
+                {!animate && <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
+                  <path strokeLinecap="round" strokeLinejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" />
+                </svg>}
+              </div>
+              <div className="pt-2">{text}</div>
+            </div>
+          </button>)}
+        </div>
+      </Card>
+    </main>
+  );
+}
diff --git a/keep-ui/app/ai/model.ts b/keep-ui/app/ai/model.ts
new file mode 100644
index 0000000000..a0d51d359a
--- /dev/null
+++ b/keep-ui/app/ai/model.ts
@@ -0,0 +1,6 @@
+export interface AIStats {
+  alerts_count: number;
+  incidents_count: number;
+  first_alert_datetime?: Date;
+  is_mining_enabled: boolean;
+}
diff --git a/keep-ui/app/ai/page.tsx b/keep-ui/app/ai/page.tsx
new file mode 100644
index 0000000000..bfb4bcde92
--- /dev/null
+++ b/keep-ui/app/ai/page.tsx
@@ -0,0 +1,11 @@
+import AI from "./ai";
+
+export default function Page() {
+  return <AI />;
+}
+
+export const metadata = {
+  title: "Keep - AI Correlation",
+  description:
+    "Map new event attributes from existing event attributes with Regex",
+};
diff --git a/keep-ui/app/alerts/alert-actions.tsx b/keep-ui/app/alerts/alert-actions.tsx
index 178bdfeabe..f636500ba7 100644
--- a/keep-ui/app/alerts/alert-actions.tsx
+++ b/keep-ui/app/alerts/alert-actions.tsx
@@ -1,3 +1,4 @@
+import { useState } from "react";
 import { Button } from "@tremor/react";
 import { getSession } from "next-auth/react";
 import { getApiURL } from "utils/apiUrl";
@@ -7,6 +8,7 @@ import { toast } from "react-toastify";
 import { usePresets } from "utils/hooks/usePresets";
 import { useRouter } from "next/navigation";
 import { SilencedDoorbellNotification } from "@/components/icons";
+import AlertAssociateIncidentModal from "./alert-associate-incident-modal";
 
 interface Props {
   selectedRowIds: string[];
@@ -26,6 +28,7 @@ export default function AlertActions({
   const { mutate: presetsMutator } = useAllPresets({
     revalidateOnFocus: false,
   });
+  const [isIncidentSelectorOpen, setIsIncidentSelectorOpen] = useState<boolean>(false);
 
   const selectedAlerts = alerts.filter((_alert, index) =>
     selectedRowIds.includes(index.toString())
@@ -75,6 +78,18 @@ export default function AlertActions({
     }
   }
 
+  const showIncidentSelector = () => {
+    setIsIncidentSelectorOpen(true);
+  }
+  const hideIncidentSelector = () => {
+    setIsIncidentSelectorOpen(false);
+  }
+
+  const handleSuccessfulAlertsAssociation = () => {
+    hideIncidentSelector();
+    clearRowSelection();
+  }
+
   return (
     <div className="w-full flex justify-end items-center">
       <Button
@@ -99,6 +114,21 @@ export default function AlertActions({
       >
         Create Preset
       </Button>
+      <Button
+        icon={PlusIcon}
+        size="xs"
+        color="orange"
+        className="ml-2.5"
+        onClick={showIncidentSelector}
+        tooltip="Associate events with incident"
+      >
+        Associate with incident
+      </Button>
+      <AlertAssociateIncidentModal
+          isOpen={isIncidentSelectorOpen}
+          alerts={selectedAlerts}
+          handleSuccess={handleSuccessfulAlertsAssociation}
+          handleClose={hideIncidentSelector}/>
     </div>
   );
 }
diff --git a/keep-ui/app/alerts/alert-associate-incident-modal.tsx b/keep-ui/app/alerts/alert-associate-incident-modal.tsx
new file mode 100644
index 0000000000..0f391d5a0e
--- /dev/null
+++ b/keep-ui/app/alerts/alert-associate-incident-modal.tsx
@@ -0,0 +1,120 @@
+import React, {FormEvent, useState} from "react";
+import { useSession } from "next-auth/react";
+import { AlertDto } from "./models";
+import Modal from "@/components/ui/Modal";
+import { useIncidents } from "../../utils/hooks/useIncidents";
+import Loading from "../loading";
+import {Button, Divider, Select, SelectItem, Title} from "@tremor/react";
+import {useRouter} from "next/navigation";
+import {getApiURL} from "../../utils/apiUrl";
+import {toast} from "react-toastify";
+
+interface AlertAssociateIncidentModalProps {
+  isOpen: boolean;
+  handleSuccess: () => void;
+  handleClose: () => void;
+  alerts: Array<AlertDto>;
+}
+
+const AlertAssociateIncidentModal = ({
+  isOpen,
+  handleSuccess,
+  handleClose,
+  alerts,
+}: AlertAssociateIncidentModalProps) => {
+
+  const { data: incidents, isLoading, mutate } = useIncidents(true, 100);
+  const [selectedIncident, setSelectedIncident] = useState<string | null>(null);
+  // get the token
+  const { data: session } = useSession();
+  const router = useRouter();
+  // if this modal should not be open, do nothing
+  if (!alerts) return null;
+  const handleAssociateAlerts = async (e: FormEvent) => {
+    e.preventDefault();
+    const apiUrl = getApiURL();
+    const response = await fetch(
+      `${apiUrl}/incidents/${selectedIncident}/alerts`,
+      {
+        method: "POST",
+        headers: {
+          Authorization: `Bearer ${session?.accessToken}`,
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(alerts.map(({ event_id }) => event_id)),
+      });
+    if (response.ok) {
+      handleSuccess();
+      await mutate();
+      toast.success("Alerts associated with incident successfully");
+    } else {
+      toast.error(
+        "Failed to associated alerts with incident, please contact us if this issue persists."
+      );
+    }
+  }
+
+
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={handleClose}
+      title="Choose Incident"
+      className="w-[600px]"
+    >
+      <div className="relative bg-white p-6 rounded-lg">
+        {isLoading ? (
+            <Loading />
+          ) : incidents && incidents.items.length > 0 ? (
+            <div className="h-full justify-center">
+              <Select
+                className="my-2.5"
+                placeholder={`Select incident`}
+                onValueChange={(value) => setSelectedIncident(value)}
+              >
+                {
+                  incidents.items?.map((incident) => {
+
+                    return (
+                      <SelectItem
+                        key={incident.id}
+                        value={incident.id}
+                      >
+                        {incident.name}
+                      </SelectItem>
+                    );
+                  })!
+                }
+              </Select>
+              <Divider />
+              <div className="right">
+                <Button
+                  color="orange"
+                  onClick={handleAssociateAlerts}
+                  disabled={selectedIncident === null}
+                >
+                  Associate {alerts.length} alert{alerts.length > 1 ? "s" : ""}
+                </Button>
+              </div>
+
+            </div>
+          ) : (
+            <div className="flex flex-col items-center justify-center gap-y-8 h-full">
+              <div className="text-center space-y-3">
+                <Title className="text-2xl">No Incidents Yet</Title>
+              </div>
+              <Button
+                className="mb-10"
+                color="orange"
+                onClick={() => router.push("/incidents")}
+              >
+                Register Incident
+              </Button>
+            </div>
+          )}
+      </div>
+    </Modal>
+  );
+};
+
+export default AlertAssociateIncidentModal;
diff --git a/keep-ui/app/alerts/alert-graph-viz.tsx b/keep-ui/app/alerts/alert-graph-viz.tsx
index df7b96c0b8..8352c83ab2 100644
--- a/keep-ui/app/alerts/alert-graph-viz.tsx
+++ b/keep-ui/app/alerts/alert-graph-viz.tsx
@@ -170,4 +170,4 @@ const GraphVisualization: React.FC<GraphVisualizationProps> = ({ demoMode }) =>
   );
 };
 
-export default GraphVisualization;
+export default GraphVisualization;
\ No newline at end of file
diff --git a/keep-ui/app/alerts/alerts.tsx b/keep-ui/app/alerts/alerts.tsx
index 0974e61d6a..440ee0ff6c 100644
--- a/keep-ui/app/alerts/alerts.tsx
+++ b/keep-ui/app/alerts/alerts.tsx
@@ -14,7 +14,7 @@ import AlertDismissModal from "./alert-dismiss-modal";
 import { ViewAlertModal } from "./ViewAlertModal";
 import { useRouter, useSearchParams } from "next/navigation";
 import AlertChangeStatusModal from "./alert-change-status-modal";
-import { usePusher } from "utils/hooks/usePusher";
+import { useAlertPolling } from "utils/hooks/usePusher";
 
 const defaultPresets: Preset[] = [
   {
@@ -84,7 +84,7 @@ export default function Alerts({ presetName }: AlertsProps) {
   const selectedPreset = presets.find(
     (preset) => preset.name.toLowerCase() === decodeURIComponent(presetName)
   );
-  const { data: pusher } = usePusher();
+  const { data: pollAlerts } = useAlertPolling();
   const {
     data: alerts = [],
     isLoading: isAsyncLoading,
@@ -101,10 +101,10 @@ export default function Alerts({ presetName }: AlertsProps) {
   }, [searchParams, alerts]);
 
   useEffect(() => {
-    if (pusher?.pollAlerts) {
+    if (pollAlerts) {
       mutateAlerts();
     }
-  }, [mutateAlerts, pusher?.pollAlerts]);
+  }, [mutateAlerts, pollAlerts]);
 
   if (selectedPreset === undefined) {
     return null;
diff --git a/keep-ui/app/alerts/models.tsx b/keep-ui/app/alerts/models.tsx
index 4d425636ef..1347813c9e 100644
--- a/keep-ui/app/alerts/models.tsx
+++ b/keep-ui/app/alerts/models.tsx
@@ -34,6 +34,7 @@ export enum Status {
 
 export interface AlertDto {
   id: string;
+  event_id: string;
   name: string;
   status: Status;
   lastReceived: Date;
diff --git a/keep-ui/app/incidents/IncidentPlaceholder.tsx b/keep-ui/app/incidents/IncidentPlaceholder.tsx
new file mode 100644
index 0000000000..ff5a4ff8d4
--- /dev/null
+++ b/keep-ui/app/incidents/IncidentPlaceholder.tsx
@@ -0,0 +1,38 @@
+import { Fragment } from "react";
+import { Button, Subtitle, Title } from "@tremor/react";
+
+
+interface Props {
+  setIsFormOpen: (value: boolean) => void;
+}
+
+
+export const IncidentPlaceholder = ({
+  setIsFormOpen,
+}: Props) => {
+
+  const onCreateButtonClick = () => {
+    setIsFormOpen(true);
+  };
+
+  return (
+    <Fragment>
+      <div className="flex flex-col items-center justify-center gap-y-8 h-full">
+        <div className="text-center space-y-3">
+          <Title className="text-2xl">No Incidents Yet</Title>
+          <Subtitle className="text-gray-400">
+            Create incidents manually to enable AI detection
+          </Subtitle>
+        </div>
+        <Button
+          className="mb-10"
+          color="orange"
+          onClick={() => onCreateButtonClick()}
+        >
+          Create Incident
+        </Button>
+      </div>
+
+    </Fragment>
+  );
+};
diff --git a/keep-ui/app/incidents/[id]/incident-alert-menu.tsx b/keep-ui/app/incidents/[id]/incident-alert-menu.tsx
new file mode 100644
index 0000000000..aaa43ab282
--- /dev/null
+++ b/keep-ui/app/incidents/[id]/incident-alert-menu.tsx
@@ -0,0 +1,56 @@
+import { TrashIcon } from "@radix-ui/react-icons";
+import { Icon } from "@tremor/react";
+import { AlertDto } from "app/alerts/models";
+import { useSession } from "next-auth/react";
+import { toast } from "react-toastify";
+import { getApiURL } from "utils/apiUrl";
+import { useIncidentAlerts } from "utils/hooks/useIncidents";
+
+interface Props {
+  incidentId: string;
+  alert: AlertDto;
+}
+export default function IncidentAlertMenu({ incidentId, alert }: Props) {
+  const apiUrl = getApiURL();
+  const { data: session } = useSession();
+  const { mutate } = useIncidentAlerts(incidentId);
+
+  function onRemove() {
+    if (confirm("Are you sure you want to remove correlation?")) {
+      fetch(`${apiUrl}/incidents/${incidentId}/alerts`, {
+        method: "DELETE",
+        headers: {
+          Authorization: `Bearer ${session?.accessToken}`,
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify([alert.event_id]),
+      }).then((response) => {
+        if (response.ok) {
+          toast.success("Alert removed from incident successfully", {
+            position: "top-right",
+          });
+          mutate();
+        } else {
+          toast.error(
+            "Failed to remove alert from incident, please contact us if this issue persists.",
+            {
+              position: "top-right",
+            }
+          );
+        }
+      });
+    }
+  }
+
+  return (
+    <div className="flex flex-col">
+      <Icon
+        icon={TrashIcon}
+        color="red"
+        tooltip="Remove"
+        className="cursor-pointer"
+        onClick={onRemove}
+      />
+    </div>
+  );
+}
diff --git a/keep-ui/app/incidents/[id]/incident-alerts.tsx b/keep-ui/app/incidents/[id]/incident-alerts.tsx
new file mode 100644
index 0000000000..a6058720f1
--- /dev/null
+++ b/keep-ui/app/incidents/[id]/incident-alerts.tsx
@@ -0,0 +1,174 @@
+import {
+  createColumnHelper,
+  flexRender,
+  getCoreRowModel,
+  useReactTable,
+} from "@tanstack/react-table";
+import {
+  Callout,
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeaderCell,
+  TableRow,
+} from "@tremor/react";
+import Image from "next/image";
+import AlertSeverity from "app/alerts/alert-severity";
+import { AlertDto } from "app/alerts/models";
+import Skeleton from "react-loading-skeleton";
+import "react-loading-skeleton/dist/skeleton.css";
+import { getAlertLastReceieved } from "utils/helpers";
+import {
+  useIncidentAlerts,
+  usePollIncidentAlerts,
+} from "utils/hooks/useIncidents";
+import AlertName from "app/alerts/alert-name";
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import IncidentAlertMenu from "./incident-alert-menu";
+
+interface Props {
+  incidentId: string;
+}
+
+const columnHelper = createColumnHelper<AlertDto>();
+
+export default function IncidentAlerts({ incidentId }: Props) {
+  const { data: alerts } = useIncidentAlerts(incidentId);
+  usePollIncidentAlerts(incidentId);
+
+  const columns = [
+    columnHelper.accessor("severity", {
+      id: "severity",
+      header: "Severity",
+      minSize: 100,
+      cell: (context) => <AlertSeverity severity={context.getValue()} />,
+    }),
+    columnHelper.display({
+      id: "name",
+      header: "Name",
+      minSize: 330,
+      cell: (context) => <AlertName alert={context.row.original} />,
+    }),
+    columnHelper.accessor("description", {
+      id: "description",
+      header: "Description",
+      minSize: 100,
+      cell: (context) => (
+        <div title={context.getValue()}>
+          <div className="truncate">{context.getValue()}</div>
+        </div>
+      ),
+    }),
+    columnHelper.accessor("status", {
+      id: "status",
+      minSize: 100,
+      header: "Status",
+    }),
+    columnHelper.accessor("lastReceived", {
+      id: "lastReceived",
+      header: "Last Received",
+      minSize: 100,
+      cell: (context) => (
+        <span>{getAlertLastReceieved(context.getValue())}</span>
+      ),
+    }),
+    columnHelper.accessor("source", {
+      id: "source",
+      header: "Source",
+      minSize: 100,
+      cell: (context) =>
+        (context.getValue() ?? []).map((source, index) => (
+          <Image
+            className={`inline-block ${index == 0 ? "" : "-ml-2"}`}
+            key={source}
+            alt={source}
+            height={24}
+            width={24}
+            title={source}
+            src={`/icons/${source}-icon.png`}
+          />
+        )),
+    }),
+    columnHelper.display({
+      id: "remove",
+      header: "",
+      cell: (context) => (
+        <IncidentAlertMenu
+          alert={context.row.original}
+          incidentId={incidentId}
+        />
+      ),
+    }),
+  ];
+
+  const table = useReactTable({
+    columns: columns,
+    data: alerts ?? [],
+    getCoreRowModel: getCoreRowModel(),
+  });
+  return (
+    <>
+      {(alerts ?? []).length === 0 && (
+        <Callout
+          className="mt-4 w-full"
+          title="Missing Alerts"
+          icon={ExclamationTriangleIcon}
+          color={"orange"}
+        >
+          Alerts will show up here as they are correlated into this incident.
+        </Callout>
+      )}
+
+      <Table>
+        <TableHead>
+          {table.getHeaderGroups().map((headerGroup) => (
+            <TableRow key={headerGroup.id}>
+              {headerGroup.headers.map((header) => {
+                return (
+                  <TableHeaderCell key={header.id}>
+                    {flexRender(
+                      header.column.columnDef.header,
+                      header.getContext()
+                    )}
+                  </TableHeaderCell>
+                );
+              })}
+            </TableRow>
+          ))}
+        </TableHead>
+        {alerts && alerts.length > 0 && (
+          <TableBody>
+            {table.getRowModel().rows.map((row) => (
+              <TableRow key={row.id} className="hover:bg-slate-100">
+                {row.getVisibleCells().map((cell) => (
+                  <TableCell key={cell.id}>
+                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                  </TableCell>
+                ))}
+              </TableRow>
+            ))}
+          </TableBody>
+        )}
+        {
+          // Skeleton
+          (alerts ?? []).length === 0 && (
+            <TableBody>
+              {Array(5)
+                .fill("")
+                .map((index) => (
+                  <TableRow key={index}>
+                    {columns.map((c) => (
+                      <TableCell key={c.id}>
+                        <Skeleton />
+                      </TableCell>
+                    ))}
+                  </TableRow>
+                ))}
+            </TableBody>
+          )
+        }
+      </Table>
+    </>
+  );
+}
diff --git a/keep-ui/app/incidents/[id]/incident-info.tsx b/keep-ui/app/incidents/[id]/incident-info.tsx
new file mode 100644
index 0000000000..08b3be466c
--- /dev/null
+++ b/keep-ui/app/incidents/[id]/incident-info.tsx
@@ -0,0 +1,34 @@
+import { Callout, Title } from "@tremor/react";
+import { IncidentDto } from "../model";
+import { RiSparkling2Line } from "react-icons/ri";
+
+interface Props {
+  incident: IncidentDto;
+}
+
+export default function IncidentInformation({ incident }: Props) {
+  return (
+    <div className="flex h-full flex-col justify-between">
+      <div>
+        <Title className="mb-2.5">⚔️ Incident Information</Title>
+        <div className="prose-2xl">{incident.name}</div>
+        <p>Description: {incident.description}</p>
+        <p>Started at: {incident.start_time?.toISOString() ?? "N/A"}</p>
+        <Callout
+          title="AI Summary"
+          color="gray"
+          icon={RiSparkling2Line}
+          className="mt-10 mb-10"
+        >
+          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
+          minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+          aliquip ex ea commodo consequat. Duis aute irure dolor in
+          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
+          culpa qui officia deserunt mollit anim id est laborum.
+        </Callout>
+      </div>
+    </div>
+  );
+}
diff --git a/keep-ui/app/incidents/[id]/incident.tsx b/keep-ui/app/incidents/[id]/incident.tsx
new file mode 100644
index 0000000000..770db93d97
--- /dev/null
+++ b/keep-ui/app/incidents/[id]/incident.tsx
@@ -0,0 +1,75 @@
+"use client";
+import Loading from "app/loading";
+import { useIncident } from "utils/hooks/useIncidents";
+import IncidentInformation from "./incident-info";
+import {
+  Card,
+  Icon,
+  Subtitle,
+  Tab,
+  TabGroup,
+  TabList,
+  TabPanel,
+  TabPanels,
+  Title,
+} from "@tremor/react";
+import IncidentAlerts from "./incident-alerts";
+import { ArrowUturnLeftIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/navigation";
+
+interface Props {
+  incidentId: string;
+}
+
+export default function IncidentView({ incidentId }: Props) {
+  const { data: incident, isLoading, error } = useIncident(incidentId);
+  const router = useRouter();
+
+  if (isLoading || !incident) return <Loading />;
+  if (error) return <Title>Incident does not exist.</Title>;
+
+  return (
+    <>
+      <div className="flex justify-between items-center">
+        <div>
+          <Title>Incident Management</Title>
+          <Subtitle>
+            Understand, manage and triage your incidents faster with Keep.
+          </Subtitle>
+        </div>
+        <Icon
+          icon={ArrowUturnLeftIcon}
+          tooltip="Go Back"
+          variant="shadow"
+          className="cursor-pointer"
+          onClick={() => router.back()}
+        />
+      </div>
+      <Card className="flex flex-col items-center justify-center gap-y-8 mt-10 p-4 md:p-10 mx-auto">
+        <div className="w-full">
+          <div className="flex divide-x p-2">
+            <div id="incidentOverview" className="w-2/5 min-w-[400px] pr-2.5">
+              <IncidentInformation incident={incident} />
+            </div>
+            <div id="incidentTabs" className="w-full pl-2.5 overflow-x-scroll">
+              <TabGroup defaultIndex={0}>
+                <TabList variant="line" color="orange">
+                  <Tab>Alerts</Tab>
+                  <Tab>Timeline</Tab>
+                  <Tab>Topology</Tab>
+                </TabList>
+                <TabPanels>
+                  <TabPanel>
+                    <IncidentAlerts incidentId={incident.id} />
+                  </TabPanel>
+                  <TabPanel>Coming Soon...</TabPanel>
+                  <TabPanel>Coming Soon...</TabPanel>
+                </TabPanels>
+              </TabGroup>
+            </div>
+          </div>
+        </div>
+      </Card>
+    </>
+  );
+}
diff --git a/keep-ui/app/incidents/[id]/layout.tsx b/keep-ui/app/incidents/[id]/layout.tsx
new file mode 100644
index 0000000000..6f304e5302
--- /dev/null
+++ b/keep-ui/app/incidents/[id]/layout.tsx
@@ -0,0 +1,3 @@
+export default function Layout({ children }: { children: any }) {
+  return <main className="p-4 md:p-10 mx-auto max-w-full">{children}</main>;
+}
diff --git a/keep-ui/app/incidents/[id]/page.tsx b/keep-ui/app/incidents/[id]/page.tsx
new file mode 100644
index 0000000000..73ff6ffbb2
--- /dev/null
+++ b/keep-ui/app/incidents/[id]/page.tsx
@@ -0,0 +1,15 @@
+import IncidentView from "./incident";
+
+type PageProps = {
+  params: { id: string };
+  searchParams: { [key: string]: string | string[] | undefined };
+};
+
+export const metadata = {
+  title: "Keep - Incidents",
+  description: "List of incidents",
+};
+
+export default function Page(props: PageProps) {
+  return <IncidentView incidentId={props.params.id} />;
+}
diff --git a/keep-ui/app/incidents/create-or-update-incident.tsx b/keep-ui/app/incidents/create-or-update-incident.tsx
new file mode 100644
index 0000000000..da35b65e21
--- /dev/null
+++ b/keep-ui/app/incidents/create-or-update-incident.tsx
@@ -0,0 +1,182 @@
+"use client";
+
+import {
+  TextInput,
+  Textarea,
+  Divider,
+  Subtitle,
+  Text,
+  Button,
+} from "@tremor/react";
+import { useSession } from "next-auth/react";
+import { FormEvent, useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { getApiURL } from "utils/apiUrl";
+import { IncidentDto } from "./model";
+import { useIncidents } from "utils/hooks/useIncidents";
+
+interface Props {
+  incidentToEdit: IncidentDto | null;
+  editCallback: (rule: IncidentDto | null) => void;
+}
+
+export default function CreateOrUpdateIncident({
+  incidentToEdit,
+  editCallback,
+}: Props) {
+  const { data: session } = useSession();
+  const { mutate } = useIncidents(true, 20);
+  const [incidentName, setIncidentName] = useState<string>("");
+  const [incidentDescription, setIncidentDescription] = useState<string>("");
+  const [incidentAssignee, setIncidentAssignee] = useState<string>("");
+  const editMode = incidentToEdit !== null;
+
+
+  useEffect(() => {
+    if (incidentToEdit) {
+      setIncidentName(incidentToEdit.name);
+      setIncidentDescription(incidentToEdit.description ?? "");
+      setIncidentAssignee(incidentToEdit.assignee ?? "");
+    }
+  }, [incidentToEdit]);
+
+  const clearForm = () => {
+    setIncidentName("");
+    setIncidentDescription("");
+    setIncidentAssignee("");
+  };
+
+  const addIncident = async (e: FormEvent) => {
+    e.preventDefault();
+    const apiUrl = getApiURL();
+    const response = await fetch(`${apiUrl}/incidents`, {
+      method: "POST",
+      headers: {
+        Authorization: `Bearer ${session?.accessToken}`,
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+        name: incidentName,
+        description: incidentDescription,
+        assignee: incidentAssignee,
+      }),
+    });
+    if (response.ok) {
+      exitEditMode();
+      await mutate();
+      toast.success("Incident created successfully");
+    } else {
+      toast.error(
+        "Failed to create incident, please contact us if this issue persists."
+      );
+    }
+  };
+
+  // This is the function that will be called on submitting the form in the editMode, it sends a PUT request to the backend.
+  const updateIncident = async (e: FormEvent) => {
+    e.preventDefault();
+    const apiUrl = getApiURL();
+    const response = await fetch(
+      `${apiUrl}/incidents/${incidentToEdit?.id}`,
+      {
+        method: "PUT",
+        headers: {
+          Authorization: `Bearer ${session?.accessToken}`,
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          name: incidentName,
+          description: incidentDescription,
+          assignee: incidentAssignee,
+        }),
+      }
+    );
+    if (response.ok) {
+      exitEditMode();
+      await mutate();
+      toast.success("Incident updated successfully");
+    } else {
+      toast.error(
+        "Failed to update incident, please contact us if this issue persists."
+      );
+    }
+  };
+
+  // If the Incident is successfully updated or the user cancels the update we exit the editMode and set the editRule in the incident.tsx to null.
+  const exitEditMode = () => {
+    editCallback(null);
+    clearForm();
+  };
+
+  const submitEnabled = (): boolean => {
+    return (
+      !!incidentName &&
+      !!incidentDescription
+    );
+  };
+
+  return (
+    <form
+      className="py-2"
+      onSubmit={editMode ? updateIncident : addIncident}
+    >
+      <Subtitle>Incident Metadata</Subtitle>
+      <div className="mt-2.5">
+        <Text>
+          Name<span className="text-red-500 text-xs">*</span>
+        </Text>
+        <TextInput
+          placeholder="Incident Name"
+          required={true}
+          value={incidentName}
+          onValueChange={setIncidentName}
+        />
+      </div>
+      <div className="mt-2.5">
+        <Text>Description<span className="text-red-500 text-xs">*</span></Text>
+        <Textarea
+          placeholder="What happened?"
+          required={true}
+          value={incidentDescription}
+          onValueChange={setIncidentDescription}
+        />
+      </div>
+
+      <div className="mt-2.5">
+        <Text>Assignee</Text>
+        <TextInput
+          placeholder="Who is responsible"
+          value={incidentAssignee}
+          onValueChange={setIncidentAssignee}
+        />
+      </div>
+
+      <Divider />
+
+
+      <div className={"space-x-1 flex flex-row justify-end items-center"}>
+        {/*If we are in the editMode we need an extra cancel button option for the user*/}
+        {editMode ? (
+          <Button
+            color="orange"
+            size="xs"
+            variant="secondary"
+            onClick={exitEditMode}
+          >
+            Cancel
+          </Button>
+        ) : (
+          <></>
+        )}
+        <Button
+          disabled={!submitEnabled()}
+          color="orange"
+          size="xs"
+          type="submit"
+        >
+          {editMode ? "Update" : "Create"}
+        </Button>
+      </div>
+    </form>
+  );
+}
diff --git a/keep-ui/app/incidents/incident-pagination.tsx b/keep-ui/app/incidents/incident-pagination.tsx
new file mode 100644
index 0000000000..9c5e0913c2
--- /dev/null
+++ b/keep-ui/app/incidents/incident-pagination.tsx
@@ -0,0 +1,118 @@
+import {
+  ArrowPathIcon,
+  ChevronDoubleLeftIcon,
+  ChevronDoubleRightIcon,
+  ChevronLeftIcon,
+  ChevronRightIcon,
+  TableCellsIcon,
+} from "@heroicons/react/24/outline";
+import { Button, Text } from "@tremor/react";
+import { StylesConfig, SingleValueProps, components, GroupBase } from 'react-select';
+import Select from 'react-select';
+import { Table } from "@tanstack/react-table";
+import {IncidentDto} from "./model";
+
+interface Props {
+  table: Table<IncidentDto>;
+  isRefreshAllowed: boolean;
+}
+
+interface OptionType {
+  value: string;
+  label: string;
+}
+
+  const customStyles: StylesConfig<OptionType, false, GroupBase<OptionType>> = {
+    control: (provided, state) => ({
+      ...provided,
+      borderColor: state.isFocused ? 'orange' : provided.borderColor,
+      '&:hover': { borderColor: 'orange' },
+      boxShadow: state.isFocused ? '0 0 0 1px orange' : provided.boxShadow,
+    }),
+    singleValue: (provided) => ({
+      ...provided,
+      display: 'flex',
+      alignItems: 'center',
+    }),
+    menu: (provided) => ({
+      ...provided,
+      color: 'orange',
+    }),
+    option: (provided, state) => ({
+      ...provided,
+      backgroundColor: state.isSelected ? 'orange' : provided.backgroundColor,
+      '&:hover': { backgroundColor: state.isSelected ? 'orange' : '#f5f5f5' },
+      color: state.isSelected ? 'white' : provided.color,
+    }),
+  };
+
+  const SingleValue = ({ children, ...props }: SingleValueProps<OptionType, false, GroupBase<OptionType>>) => (
+    <components.SingleValue {...props}>
+      {children}
+      <TableCellsIcon className="w-4 h-4 ml-2" />
+    </components.SingleValue>
+  );
+
+
+export default function IncidentPagination({  table, isRefreshAllowed }: Props) {
+
+  const pageIndex = table.getState().pagination.pageIndex;
+  const pageCount = table.getPageCount();
+
+  return (
+    <div className="flex justify-between items-center">
+      <Text>
+        Showing {pageCount === 0 ? 0 : pageIndex + 1} of {pageCount}
+      </Text>
+      <div className="flex gap-1">
+      <Select
+        styles={customStyles}
+        components={{ SingleValue }}
+         value={{ value: table.getState().pagination.pageSize.toString(), label: table.getState().pagination.pageSize.toString() }}
+         onChange={(selectedOption) => table.setPageSize(Number(selectedOption!.value))}
+         options={[
+           { value: "10", label: "10" },
+           { value: "20", label: "20" },
+           { value: "50", label: "50" },
+           { value: "100", label: "100" },
+         ]}
+         menuPlacement="top"
+        />
+        <div className="flex">
+          <Button
+            icon={ChevronDoubleLeftIcon}
+            onClick={() => table.setPageIndex(0)}
+            disabled={!table.getCanPreviousPage()}
+            size="xs"
+            color="orange"
+            variant="secondary"
+          />
+          <Button
+            icon={ChevronLeftIcon}
+            onClick={table.previousPage}
+            disabled={!table.getCanPreviousPage()}
+            size="xs"
+            color="orange"
+            variant="secondary"
+          />
+          <Button
+            icon={ChevronRightIcon}
+            onClick={table.nextPage}
+            disabled={!table.getCanNextPage()}
+            size="xs"
+            color="orange"
+            variant="secondary"
+          />
+          <Button
+            icon={ChevronDoubleRightIcon}
+            onClick={() => table.setPageIndex(pageCount - 1)}
+            disabled={!table.getCanNextPage()}
+            size="xs"
+            color="orange"
+            variant="secondary"
+          />
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/keep-ui/app/incidents/incident.tsx b/keep-ui/app/incidents/incident.tsx
new file mode 100644
index 0000000000..e76f07ada8
--- /dev/null
+++ b/keep-ui/app/incidents/incident.tsx
@@ -0,0 +1,123 @@
+"use client";
+import { Card, Title, Subtitle, Button } from "@tremor/react";
+import Loading from "app/loading";
+import { useState } from "react";
+import { IncidentDto } from "./model";
+import CreateOrUpdateIncident from "./create-or-update-incident";
+import IncidentsTable from "./incidents-table";
+import { useIncidents, usePollIncidents } from "utils/hooks/useIncidents";
+import { IncidentPlaceholder } from "./IncidentPlaceholder";
+import Modal from "@/components/ui/Modal";
+import { PlusCircleIcon } from "@heroicons/react/24/outline";
+import PredictedIncidentsTable from "./predicted-incidents-table";
+
+interface Pagination {
+  limit: number;
+  offset: number;
+}
+
+export default function Incident() {
+  const [incidentsPagination, setIncidentsPagination] = useState<Pagination>({
+    limit: 20,
+    offset: 0,
+  });
+
+  const { data: incidents, isLoading,
+    mutate: mutateIncidents} = useIncidents(
+      true,
+      incidentsPagination.limit,
+      incidentsPagination.offset
+  );
+  const { data: predictedIncidents, isLoading: isPredictedLoading,
+    mutate: mutatePredictedIncidents } = useIncidents(false);
+  usePollIncidents(mutateIncidents);
+
+  const [incidentToEdit, setIncidentToEdit] =
+    useState<IncidentDto | null>(null);
+
+  const [isFormOpen, setIsFormOpen] = useState<boolean>(false);
+
+  const handleCloseForm = () => {
+    setIsFormOpen(false);
+  };
+
+  const handleStartEdit = (incident: IncidentDto) => {
+    setIncidentToEdit(incident);
+    setIsFormOpen(true);
+  };
+
+  const handleFinishEdit = () => {
+    setIncidentToEdit(null);
+    setIsFormOpen(false);
+  };
+
+  return (
+    <div className="flex h-full w-full">
+      <div className="flex-grow overflow-auto p-2.5">
+        {!isPredictedLoading && predictedIncidents && predictedIncidents.items.length > 0 ?
+          <Card className="mt-10 mb-10 flex-grow">
+            <Title>Incident Predictions</Title>
+            <Subtitle>Possible problems predicted by Keep AI <Badge color="orange">Beta</Badge></Subtitle>
+            <PredictedIncidentsTable
+              incidents={predictedIncidents}
+              mutate={async () => {
+                await mutatePredictedIncidents();
+                await mutateIncidents();
+              }}
+              editCallback={handleStartEdit}
+            />
+          </Card>
+        : null
+        }
+
+        {isLoading ? (
+          <Loading />
+        ) : incidents && incidents.items.length > 0 ? (
+          <div className="h-full flex flex-col">
+            <div className="flex justify-between items-center">
+              <div>
+                <Title>Incidents</Title>
+                <Subtitle>Manage your incidents</Subtitle>
+              </div>
+              <div>
+                <Button
+                  color="orange"
+                  size="md"
+                  icon={PlusCircleIcon}
+                  onClick={() => setIsFormOpen(true)}
+                >
+                  Create Incident
+                </Button>
+              </div>
+            </div>
+            <Card className="mt-10 flex-grow">
+              <IncidentsTable
+                incidents={incidents}
+                mutate={mutateIncidents}
+                setPagination={setIncidentsPagination}
+                editCallback={handleStartEdit}
+              />
+            </Card>
+          </div>
+        ) : (
+          <div className="h-full flex">
+            <Card className="flex-grow flex items-center justify-center">
+              <IncidentPlaceholder setIsFormOpen={setIsFormOpen} />
+            </Card>
+          </div>
+        )}
+      </div>
+      <Modal
+        isOpen={isFormOpen}
+        onClose={handleCloseForm}
+        className="w-[600px]"
+        title="Add Incident"
+      >
+        <CreateOrUpdateIncident
+          incidentToEdit={incidentToEdit}
+          editCallback={handleFinishEdit}
+        />
+      </Modal>
+    </div>
+  );
+}
diff --git a/keep-ui/app/incidents/incidents-table.tsx b/keep-ui/app/incidents/incidents-table.tsx
new file mode 100644
index 0000000000..e3da024fcb
--- /dev/null
+++ b/keep-ui/app/incidents/incidents-table.tsx
@@ -0,0 +1,246 @@
+import {
+  Button,
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeaderCell,
+  TableRow,
+  Badge,
+} from "@tremor/react";
+import {
+  DisplayColumnDef,
+  ExpandedState,
+  createColumnHelper,
+  flexRender,
+  getCoreRowModel,
+  useReactTable,
+} from "@tanstack/react-table";
+import { MdRemoveCircle, MdModeEdit } from "react-icons/md";
+import { useSession } from "next-auth/react";
+import { getApiURL } from "utils/apiUrl";
+import { toast } from "react-toastify";
+import {IncidentDto, PaginatedIncidentsDto} from "./model";
+import React, {Dispatch, SetStateAction, useEffect, useState} from "react";
+import { useRouter } from "next/navigation";
+import Image from "next/image";
+import IncidentPagination from "./incident-pagination";
+
+const columnHelper = createColumnHelper<IncidentDto>();
+
+interface Props {
+  incidents: PaginatedIncidentsDto;
+  mutate: () => void;
+  setPagination: Dispatch<SetStateAction<any>>;
+  editCallback: (rule: IncidentDto) => void;
+}
+
+export default function IncidentsTable({
+  incidents: incidents,
+  mutate,
+  setPagination,
+  editCallback,
+}: Props) {
+  const router = useRouter();
+  const { data: session } = useSession();
+  const [expanded, setExpanded] = useState<ExpandedState>({});
+  const [pagination, setTablePagination] = useState({
+    pageIndex: Math.ceil(incidents.offset / incidents.limit),
+    pageSize: incidents.limit,
+  });
+
+  useEffect(() => {
+    if (incidents.limit != pagination.pageSize) {
+      setPagination({
+        limit: pagination.pageSize,
+        offset: 0,
+      })
+    }
+    const currentOffset = pagination.pageSize * pagination.pageIndex;
+    if (incidents.offset != currentOffset) {
+      setPagination({
+        limit: pagination.pageSize,
+        offset: currentOffset,
+      })
+    }
+  }, [pagination])
+
+  const columns = [
+    columnHelper.display({
+      id: "name",
+      header: "Name",
+      cell: ({ row }) => row.original.name,
+    }),
+    columnHelper.display({
+      id: "description",
+      header: "Description",
+      cell: (context) => context.row.original.description,
+    }),
+    columnHelper.display({
+      id: "severity",
+      header: "Severity",
+      cell: (context) => {
+        const severity = context.row.original.severity;
+        let color;
+        if (severity === "critical") color = "red";
+        else if (severity === "info") color = "blue";
+        else if (severity === "warning") color = "yellow";
+        return <Badge color={color}>{severity}</Badge>;
+      },
+    }),
+    columnHelper.display({
+      id: "alert_count",
+      header: "Number of Alerts",
+      cell: (context) => context.row.original.number_of_alerts,
+    }),
+    columnHelper.display({
+      id: "alert_sources",
+      header: "Alert Sources",
+      cell: (context) =>
+        context.row.original.alert_sources.map((alert_sources, index) => (
+          <Image
+            className={`inline-block ${index == 0 ? "" : "-ml-2"}`}
+            key={alert_sources}
+            alt={alert_sources}
+            height={24}
+            width={24}
+            title={alert_sources}
+            src={`/icons/${alert_sources}-icon.png`}
+          />
+        )),
+    }),
+    columnHelper.display({
+      id: "services",
+      header: "Involved Services",
+      cell: (context) =>
+        context.row.original.services.map((service) => (
+          <Badge className="mr-1" key={service}>
+            {service}
+          </Badge>
+        )),
+    }),
+    columnHelper.display({
+      id: "assignee",
+      header: "Assignee",
+      cell: (context) => context.row.original.assignee,
+    }),
+    columnHelper.display({
+      id: "created_at",
+      header: "Created At",
+      cell: ({ row }) =>
+        new Date(row.original.creation_time + "Z").toLocaleString(),
+    }),
+    columnHelper.display({
+      id: "delete",
+      header: "",
+      cell: (context) => (
+        <div className={"space-x-1 flex flex-row items-center justify-center"}>
+          {/*If user wants to edit the mapping. We use the callback to set the data in mapping.tsx which is then passed to the create-new-mapping.tsx form*/}
+          <Button
+            color="orange"
+            size="xs"
+            variant="secondary"
+            icon={MdModeEdit}
+            onClick={(e: React.MouseEvent) => {
+              e.preventDefault();
+              e.stopPropagation();
+              editCallback(context.row.original!);
+            }}
+          />
+          <Button
+            color="red"
+            size="xs"
+            variant="secondary"
+            icon={MdRemoveCircle}
+            onClick={(e: React.MouseEvent) => {
+              e.preventDefault();
+              e.stopPropagation();
+              deleteIncident(context.row.original.id!);
+            }}
+          />
+        </div>
+      ),
+    }),
+  ] as DisplayColumnDef<IncidentDto>[];
+
+  const table = useReactTable({
+    columns,
+    data: incidents.items,
+    state: { expanded, pagination},
+    getCoreRowModel: getCoreRowModel(),
+    manualPagination: true,
+    rowCount: incidents.count,
+    onPaginationChange: setTablePagination,
+    onExpandedChange: setExpanded,
+  });
+
+  const deleteIncident = (incidentId: string) => {
+    const apiUrl = getApiURL();
+    if (confirm("Are you sure you want to delete this incident?")) {
+      fetch(`${apiUrl}/incidents/${incidentId}`, {
+        method: "DELETE",
+        headers: {
+          Authorization: `Bearer ${session?.accessToken}`,
+        },
+      }).then((response) => {
+        if (response.ok) {
+          mutate();
+          toast.success("Incident deleted successfully");
+        } else {
+          toast.error("Failed to delete incident, contact us if this persists");
+        }
+      });
+    }
+  };
+
+  return (
+    <div>
+      <Table className="mt-4 [&>table]:table-fixed [&>table]:w-full h-full">
+        <TableHead>
+          {table.getHeaderGroups().map((headerGroup) => (
+            <TableRow
+              className="border-b border-tremor-border dark:border-dark-tremor-border"
+              key={headerGroup.id}
+            >
+              {headerGroup.headers.map((header) => {
+                return (
+                  <TableHeaderCell
+                    className="text-tremor-content-strong dark:text-dark-tremor-content-strong"
+                    key={header.id}
+                  >
+                    {flexRender(
+                      header.column.columnDef.header,
+                      header.getContext()
+                    )}
+                  </TableHeaderCell>
+                );
+              })}
+            </TableRow>
+          ))}
+        </TableHead>
+        <TableBody>
+          {table.getRowModel().rows.map((row) => (
+            <>
+              <TableRow
+                className="even:bg-tremor-background-muted even:dark:bg-dark-tremor-background-muted hover:bg-slate-100 cursor-pointer"
+                key={row.id}
+                onClick={() => {
+                  router.push(`/incidents/${row.original.id}`);
+                }}
+              >
+                {row.getVisibleCells().map((cell) => (
+                  <TableCell key={cell.id}>
+                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                  </TableCell>
+                ))}
+              </TableRow>
+            </>
+          ))}
+        </TableBody>
+      </Table>
+      <div className="mt-4 mb-8">
+        <IncidentPagination table={table}  isRefreshAllowed={true}/>
+      </div>
+    </div>
+  );
+}
diff --git a/keep-ui/app/incidents/model.ts b/keep-ui/app/incidents/model.ts
new file mode 100644
index 0000000000..d58d08d4cd
--- /dev/null
+++ b/keep-ui/app/incidents/model.ts
@@ -0,0 +1,20 @@
+export interface IncidentDto {
+  id: string;
+  name: string;
+  description: string;
+  assignee: string;
+  severity: string;
+  number_of_alerts: number;
+  alert_sources: string[];
+  services: string[];
+  start_time?: Date;
+  end_time?: Date;
+  creation_time: Date;
+}
+
+export interface PaginatedIncidentsDto {
+  limit: number;
+  offset: number;
+  count: number;
+  items: IncidentDto[];
+}
diff --git a/keep-ui/app/incidents/page.tsx b/keep-ui/app/incidents/page.tsx
new file mode 100644
index 0000000000..f9f5471d56
--- /dev/null
+++ b/keep-ui/app/incidents/page.tsx
@@ -0,0 +1,11 @@
+import Incident from "./incident";
+
+export default function Page() {
+  return <Incident />;
+}
+
+export const metadata = {
+  title: "Keep - Incidents",
+  description:
+    "List of incidents",
+};
diff --git a/keep-ui/app/incidents/predicted-incidents-table.tsx b/keep-ui/app/incidents/predicted-incidents-table.tsx
new file mode 100644
index 0000000000..af5f021c63
--- /dev/null
+++ b/keep-ui/app/incidents/predicted-incidents-table.tsx
@@ -0,0 +1,222 @@
+import {
+  Button,
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeaderCell,
+  TableRow,
+  Badge
+} from "@tremor/react";
+import {
+  DisplayColumnDef,
+  ExpandedState,
+  createColumnHelper,
+  flexRender,
+  getCoreRowModel,
+  useReactTable,
+} from "@tanstack/react-table";
+import {MdRemoveCircle, MdModeEdit, MdDone, MdBlock} from "react-icons/md";
+import { useSession } from "next-auth/react";
+import { getApiURL } from "utils/apiUrl";
+import { toast } from "react-toastify";
+import {IncidentDto, PaginatedIncidentsDto} from "./model";
+import React, { useState } from "react";
+import { useIncidents } from "utils/hooks/useIncidents";
+import { useRouter } from "next/navigation";
+import Image from "next/image";
+
+const columnHelper = createColumnHelper<IncidentDto>();
+
+interface Props {
+  incidents: PaginatedIncidentsDto;
+  mutate: () => void;
+  editCallback: (rule: IncidentDto) => void;
+}
+
+export default function PredictedIncidentsTable({
+  incidents: incidents,
+  mutate,
+  editCallback,
+}: Props) {
+  const { data: session } = useSession();
+  const [expanded, setExpanded] = useState<ExpandedState>({});
+
+  const handleConfirmPredictedIncident = async (incidentId: string) => {
+    const apiUrl = getApiURL();
+    const response = await fetch(
+      `${apiUrl}/incidents/${incidentId}/confirm`,
+      {
+        method: "POST",
+        headers: {
+          Authorization: `Bearer ${session?.accessToken}`,
+          "Content-Type": "application/json",
+        },
+      }
+    );
+    if (response.ok) {
+      await mutate();
+      toast.success("Predicted incident confirmed successfully");
+    } else {
+      toast.error(
+        "Failed to confirm predicted incident, please contact us if this issue persists."
+      );
+    }
+  }
+
+  const columns = [
+    columnHelper.display({
+      id: "name",
+      header: "Name",
+      cell: ({ row }) => row.original.name,
+    }),
+    columnHelper.display({
+      id: "description",
+      header: "Description",
+      cell: (context) => context.row.original.description,
+    }),
+    columnHelper.display({
+      id: "severity",
+      header: "Severity",
+      cell: (context) => {
+        const severity = context.row.original.severity;
+        let color;
+        if (severity === "critical") color = "red";
+        else if (severity === "info") color = "blue";
+        else if (severity === "warning") color = "yellow";
+        return <Badge color={color}>{severity}</Badge>;
+      },
+    }),
+    columnHelper.display({
+      id: "alert_count",
+      header: "Number of Alerts",
+      cell: (context) => context.row.original.number_of_alerts,
+    }),
+    columnHelper.display({
+      id: "alert_sources",
+      header: "Alert Sources",
+      cell: (context) =>
+        (context.row.original.alert_sources.map((alert_sources, index) => (
+          <Image
+            className={`inline-block ${index == 0 ? "" : "-ml-2"}`}
+            key={alert_sources}
+            alt={alert_sources}
+            height={24}
+            width={24}
+            title={alert_sources}
+            src={`/icons/${alert_sources}-icon.png`}
+          />
+        ))
+    )}),
+    columnHelper.display({
+      id: "services",
+      header: "Involved Services",
+      cell: (context) => context.row.original.services.map((service) =>
+        <Badge key={service} className="mr-1">{service}</Badge>
+      ),
+    }),
+    columnHelper.display({
+      id: "delete",
+      header: "",
+      cell: (context) => (
+        <div className={"space-x-1 flex flex-row items-center justify-center"}>
+          {/*If user wants to edit the mapping. We use the callback to set the data in mapping.tsx which is then passed to the create-new-mapping.tsx form*/}
+          <Button
+            color="orange"
+            size="xs"
+            tooltip="Confirm incident"
+            variant="secondary"
+            icon={MdDone}
+            onClick={(e: React.MouseEvent) => {
+              e.preventDefault();
+              e.stopPropagation();
+              handleConfirmPredictedIncident(context.row.original.id!);
+            }}
+          />
+          <Button
+            color="red"
+            size="xs"
+            variant="secondary"
+            tooltip={"Discard"}
+            icon={MdBlock}
+            onClick={(e: React.MouseEvent) => {
+              e.preventDefault();
+              e.stopPropagation();
+              deleteIncident(context.row.original.id!);
+            }}
+          />
+        </div>
+      ),
+    }),
+  ] as DisplayColumnDef<IncidentDto>[];
+
+  const table = useReactTable({
+    columns,
+    data: incidents.items,
+    state: { expanded },
+    getCoreRowModel: getCoreRowModel(),
+    onExpandedChange: setExpanded,
+  });
+
+  const deleteIncident = (incidentFingerprint: string) => {
+    const apiUrl = getApiURL();
+    if (confirm("Are you sure you want to delete this incident?")) {
+      fetch(`${apiUrl}/incidents/${incidentFingerprint}`, {
+        method: "DELETE",
+        headers: {
+          Authorization: `Bearer ${session?.accessToken}`,
+        },
+      }).then((response) => {
+        if (response.ok) {
+          mutate();
+          toast.success("Incident deleted successfully");
+        } else {
+          toast.error("Failed to delete incident, contact us if this persists");
+        }
+      });
+    }
+  };
+
+  return (
+    <Table className="mt-4 [&>table]:table-fixed [&>table]:w-full h-full">
+      <TableHead>
+        {table.getHeaderGroups().map((headerGroup) => (
+          <TableRow
+            className="border-b border-tremor-border dark:border-dark-tremor-border"
+            key={headerGroup.id}
+          >
+            {headerGroup.headers.map((header) => {
+              return (
+                <TableHeaderCell
+                  className="text-tremor-content-strong dark:text-dark-tremor-content-strong"
+                  key={header.id}
+                >
+                  {flexRender(
+                    header.column.columnDef.header,
+                    header.getContext()
+                  )}
+                </TableHeaderCell>
+              );
+            })}
+          </TableRow>
+        ))}
+      </TableHead>
+      <TableBody>
+        {table.getRowModel().rows.map((row) => (
+          <>
+            <TableRow
+              className="even:bg-tremor-background-muted even:dark:bg-dark-tremor-background-muted hover:bg-slate-100"
+              key={row.id}
+            >
+              {row.getVisibleCells().map((cell) => (
+                <TableCell key={cell.id}>
+                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                </TableCell>
+              ))}
+            </TableRow>
+          </>
+        ))}
+      </TableBody>
+    </Table>
+  );
+}
diff --git a/keep-ui/app/mapping/rules-table.tsx b/keep-ui/app/mapping/rules-table.tsx
index feec54daae..f5ffe75290 100644
--- a/keep-ui/app/mapping/rules-table.tsx
+++ b/keep-ui/app/mapping/rules-table.tsx
@@ -14,14 +14,15 @@ import {
   createColumnHelper,
   flexRender,
   getCoreRowModel,
-  useReactTable, ExpandedState,
+  useReactTable,
+  ExpandedState,
 } from "@tanstack/react-table";
 import { MdRemoveCircle, MdModeEdit } from "react-icons/md";
 import { useSession } from "next-auth/react";
 import { getApiURL } from "utils/apiUrl";
 import { useMappings } from "utils/hooks/useMappingRules";
 import { toast } from "react-toastify";
-import {useState} from "react";
+import { useState } from "react";
 
 const columnHelper = createColumnHelper<MappingRule>();
 
@@ -153,17 +154,17 @@ export default function RulesTable({ mappings, editCallback }: Props) {
       <TableBody>
         {table.getRowModel().rows.map((row) => (
           <>
-          <TableRow
-            className="even:bg-tremor-background-muted even:dark:bg-dark-tremor-background-muted hover:bg-slate-100"
-            key={row.id}
-            onClick={() => row.toggleExpanded()}
-          >
-            {row.getVisibleCells().map((cell) => (
-              <TableCell key={cell.id}>
-                {flexRender(cell.column.columnDef.cell, cell.getContext())}
-              </TableCell>
-            ))}
-          </TableRow>
+            <TableRow
+              className="even:bg-tremor-background-muted even:dark:bg-dark-tremor-background-muted hover:bg-slate-100"
+              key={row.id}
+              onClick={() => row.toggleExpanded()}
+            >
+              {row.getVisibleCells().map((cell) => (
+                <TableCell key={cell.id}>
+                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                </TableCell>
+              ))}
+            </TableRow>
             {row.getIsExpanded() && (
               <TableRow className="pl-2.5">
                 <TableCell colSpan={columns.length}>
diff --git a/keep-ui/app/page.tsx b/keep-ui/app/page.tsx
index 6ecf5b1d2e..8c4ad84ad5 100644
--- a/keep-ui/app/page.tsx
+++ b/keep-ui/app/page.tsx
@@ -1,8 +1,8 @@
 import ProvidersPage from "./providers/page";
 
 export const metadata = {
-  title: "Keep Console",
-  description: "Alerting and on-call management for modern engineering teams.",
+  title: "Keep",
+  description: "The open-source AIOps and alert management platform.",
 };
 
 export default ProvidersPage;
diff --git a/keep-ui/app/rules/PlaceholderSankey.tsx b/keep-ui/app/rules/PlaceholderSankey.tsx
index d031239997..155fdb0d44 100644
--- a/keep-ui/app/rules/PlaceholderSankey.tsx
+++ b/keep-ui/app/rules/PlaceholderSankey.tsx
@@ -140,46 +140,46 @@ export const PlaceholderSankey = (props: any) => (
       fill="#7A7684"
     />
     <path d="M477.5 335L482.5 340L487.5 335" stroke="#7A7684" />
-    <path d="M506.358 289L506.358 62" stroke="#E8FAFA" stroke-width="18" />
+    <path d="M506.358 289L506.358 62" stroke="#E8FAFA" strokeWidth="18" />
     <path
       d="M484.164 289C484.164 175.755 433.12 179.86 433.12 62.752"
       stroke="#E8FAFA"
-      stroke-width="18"
+      strokeWidth="18"
     />
     <path
       d="M455.313 289C455.313 176.71 324.374 171.87 324.374 62"
       stroke="#E8FAFA"
-      stroke-width="29"
+      strokeWidth="29"
     />
     <path
       d="M528.551 289C528.551 176.71 625.091 171.87 625.091 62"
       stroke="#E8FAFA"
-      stroke-width="19"
+      strokeWidth="19"
     />
     <path
       d="M427.572 289C427.572 177.92 207.861 175.984 207.861 62"
       stroke="#E8FAFA"
-      stroke-width="18"
+      strokeWidth="18"
     />
     <path
       d="M550.744 289C550.744 177.92 770.455 175.984 770.455 62"
       stroke="#E8FAFA"
-      stroke-width="18"
+      strokeWidth="18"
     />
     <path
       d="M409.818 289C409.818 177.92 100.224 175.984 100.224 62"
       stroke="#E8FAFA"
-      stroke-width="11"
+      strokeWidth="11"
     />
     <path
       d="M394.282 289C394.282 177.92 17 175.984 17 62"
       stroke="#E8FAFA"
-      stroke-width="14"
+      strokeWidth="14"
     />
     <path
       d="M570.718 289C570.718 177.92 948 175.984 948 62"
       stroke="#E8FAFA"
-      stroke-width="14"
+      strokeWidth="14"
     />
     <path
       d="M454.108 369V361.38H451.12V360.54H458.08V361.38H455.104V369H454.108ZM460.748 369.108C460.172 369.108 459.668 368.984 459.236 368.736C458.812 368.48 458.484 368.12 458.252 367.656C458.02 367.184 457.904 366.628 457.904 365.988C457.904 365.34 458.02 364.784 458.252 364.32C458.484 363.856 458.812 363.5 459.236 363.252C459.668 362.996 460.172 362.868 460.748 362.868C461.332 362.868 461.836 362.996 462.26 363.252C462.692 363.5 463.024 363.856 463.256 364.32C463.496 364.784 463.616 365.34 463.616 365.988C463.616 366.628 463.496 367.184 463.256 367.656C463.024 368.12 462.692 368.48 462.26 368.736C461.836 368.984 461.332 369.108 460.748 369.108ZM460.748 368.316C461.324 368.316 461.78 368.12 462.116 367.728C462.452 367.328 462.62 366.748 462.62 365.988C462.62 365.22 462.448 364.64 462.104 364.248C461.768 363.848 461.316 363.648 460.748 363.648C460.18 363.648 459.728 363.848 459.392 364.248C459.056 364.64 458.888 365.22 458.888 365.988C458.888 366.748 459.056 367.328 459.392 367.728C459.728 368.12 460.18 368.316 460.748 368.316ZM467.209 369.108C466.617 369.108 466.165 368.94 465.853 368.604C465.541 368.26 465.385 367.744 465.385 367.056V363.732H464.209V362.976H465.385V361.332L466.357 361.056V362.976H468.085V363.732H466.357V366.948C466.357 367.428 466.437 367.772 466.597 367.98C466.765 368.18 467.013 368.28 467.341 368.28C467.493 368.28 467.629 368.268 467.749 368.244C467.869 368.212 467.977 368.176 468.073 368.136V368.952C467.961 369 467.825 369.036 467.665 369.06C467.513 369.092 467.361 369.108 467.209 369.108ZM471.56 369.108C471.024 369.108 470.556 368.984 470.156 368.736C469.756 368.48 469.444 368.12 469.22 367.656C469.004 367.192 468.896 366.636 468.896 365.988C468.896 365.332 469.004 364.772 469.22 364.308C469.444 363.844 469.756 363.488 470.156 363.24C470.556 362.992 471.024 362.868 471.56 362.868C472.104 362.868 472.572 363.008 472.964 363.288C473.364 363.56 473.628 363.932 473.756 364.404H473.612L473.744 362.976H474.68C474.656 363.208 474.632 363.444 474.608 363.684C474.592 363.916 474.584 364.144 474.584 364.368V369H473.612V367.584H473.744C473.616 368.056 473.352 368.428 472.952 368.7C472.552 368.972 472.088 369.108 471.56 369.108ZM471.752 368.316C472.328 368.316 472.784 368.12 473.12 367.728C473.456 367.328 473.624 366.748 473.624 365.988C473.624 365.22 473.456 364.64 473.12 364.248C472.784 363.848 472.328 363.648 471.752 363.648C471.184 363.648 470.728 363.848 470.384 364.248C470.048 364.64 469.88 365.22 469.88 365.988C469.88 366.748 470.048 367.328 470.384 367.728C470.728 368.12 471.184 368.316 471.752 368.316ZM476.464 369V360.168H477.436V369H476.464ZM481.72 369L485.452 360.54H486.316L490.108 369H489.076L488.02 366.588L488.464 366.84H483.292L483.772 366.588L482.74 369H481.72ZM485.872 361.68L483.928 366.24L483.64 366.024H488.104L487.888 366.24L485.896 361.68H485.872ZM491.276 369V360.168H492.248V369H491.276ZM496.812 369.108C495.868 369.108 495.124 368.836 494.58 368.292C494.036 367.74 493.764 366.976 493.764 366C493.764 365.368 493.884 364.82 494.124 364.356C494.364 363.884 494.7 363.52 495.132 363.264C495.564 363 496.06 362.868 496.62 362.868C497.172 362.868 497.636 362.984 498.012 363.216C498.388 363.448 498.676 363.78 498.876 364.212C499.076 364.636 499.176 365.14 499.176 365.724V366.084H494.508V365.472H498.54L498.336 365.628C498.336 364.988 498.192 364.488 497.904 364.128C497.616 363.768 497.188 363.588 496.62 363.588C496.02 363.588 495.552 363.8 495.216 364.224C494.88 364.64 494.712 365.204 494.712 365.916V366.024C494.712 366.776 494.896 367.348 495.264 367.74C495.64 368.124 496.164 368.316 496.836 368.316C497.196 368.316 497.532 368.264 497.844 368.16C498.164 368.048 498.468 367.868 498.756 367.62L499.092 368.304C498.828 368.56 498.492 368.76 498.084 368.904C497.684 369.04 497.26 369.108 496.812 369.108ZM500.639 369V364.428C500.639 364.188 500.631 363.944 500.615 363.696C500.607 363.448 500.591 363.208 500.567 362.976H501.503L501.623 364.44L501.455 364.452C501.535 364.092 501.675 363.796 501.875 363.564C502.075 363.332 502.311 363.16 502.583 363.048C502.855 362.928 503.139 362.868 503.435 362.868C503.555 362.868 503.659 362.872 503.747 362.88C503.843 362.888 503.931 362.908 504.011 362.94L503.999 363.804C503.879 363.764 503.767 363.74 503.663 363.732C503.567 363.716 503.455 363.708 503.327 363.708C502.975 363.708 502.667 363.792 502.403 363.96C502.147 364.128 501.951 364.344 501.815 364.608C501.687 364.872 501.623 365.152 501.623 365.448V369H500.639ZM507.475 369.108C506.883 369.108 506.431 368.94 506.119 368.604C505.807 368.26 505.651 367.744 505.651 367.056V363.732H504.475V362.976H505.651V361.332L506.623 361.056V362.976H508.351V363.732H506.623V366.948C506.623 367.428 506.703 367.772 506.863 367.98C507.031 368.18 507.279 368.28 507.607 368.28C507.759 368.28 507.895 368.268 508.015 368.244C508.135 368.212 508.243 368.176 508.339 368.136V368.952C508.227 369 508.091 369.036 507.931 369.06C507.779 369.092 507.627 369.108 507.475 369.108ZM511.538 369.108C511.058 369.108 510.61 369.044 510.194 368.916C509.778 368.78 509.434 368.592 509.162 368.352L509.498 367.668C509.794 367.9 510.114 368.072 510.458 368.184C510.81 368.296 511.174 368.352 511.55 368.352C512.03 368.352 512.39 368.268 512.63 368.1C512.878 367.924 513.002 367.684 513.002 367.38C513.002 367.148 512.922 366.964 512.762 366.828C512.61 366.684 512.358 366.572 512.006 366.492L510.89 366.264C510.378 366.152 509.994 365.964 509.738 365.7C509.482 365.428 509.354 365.084 509.354 364.668C509.354 364.316 509.446 364.008 509.63 363.744C509.814 363.472 510.082 363.26 510.434 363.108C510.786 362.948 511.198 362.868 511.67 362.868C512.11 362.868 512.514 362.936 512.882 363.072C513.258 363.2 513.57 363.388 513.818 363.636L513.47 364.308C513.23 364.076 512.954 363.904 512.642 363.792C512.338 363.672 512.022 363.612 511.694 363.612C511.222 363.612 510.866 363.708 510.626 363.9C510.394 364.084 510.278 364.328 510.278 364.632C510.278 364.864 510.35 365.056 510.494 365.208C510.646 365.352 510.878 365.46 511.19 365.532L512.306 365.76C512.85 365.88 513.254 366.068 513.518 366.324C513.79 366.572 513.926 366.908 513.926 367.332C513.926 367.692 513.826 368.008 513.626 368.28C513.426 368.544 513.146 368.748 512.786 368.892C512.434 369.036 512.018 369.108 511.538 369.108Z"
diff --git a/keep-ui/components/icons/index.tsx b/keep-ui/components/icons/index.tsx
index 41ed99659b..2ef207ace6 100644
--- a/keep-ui/components/icons/index.tsx
+++ b/keep-ui/components/icons/index.tsx
@@ -195,19 +195,19 @@ const ExportIcon = () => (
   >
     <path
       d="M4.5 1.875V1H1.4375C1.19586 1 1 1.19586 1 1.4375V4.5H1.875V2.49188L5.93937 6.55625L6.55625 5.93937L2.49188 1.875H4.5Z"
-      stroke-width="0.8"
+      strokeWidth="0.8"
     />
     <path
       d="M14.5626 1H11.5001V1.875H13.5082L9.44385 5.93937L10.0607 6.55625L14.1251 2.49188V4.5H15.0001V1.4375C15.0001 1.19586 14.8042 1 14.5626 1Z"
-      stroke-width="0.8"
+      strokeWidth="0.8"
     />
     <path
       d="M14.1251 13.5077L10.0607 9.44336L9.44385 10.0602L13.5082 14.1246H11.5001V14.9996H14.5626C14.8042 14.9996 15.0001 14.8037 15.0001 14.5621V11.4996H14.1251V13.5077Z"
-      stroke-width="0.8"
+      strokeWidth="0.8"
     />
     <path
       d="M5.93937 9.43945L1.875 13.5082V11.5001H1V14.5626C1 14.8042 1.19586 15.0001 1.4375 15.0001H4.5V14.1251H2.49188L6.55625 10.0607L5.93937 9.43945Z"
-      stroke-width="0.8"
+      strokeWidth="0.8"
     />
   </svg>
 );
@@ -254,5 +254,5 @@ export {
   DoorbellNotification,
   SilencedDoorbellNotification,
   Trashcan,
-  ExportIcon
+  ExportIcon,
 };
diff --git a/keep-ui/components/navbar/AILink.tsx b/keep-ui/components/navbar/AILink.tsx
new file mode 100644
index 0000000000..9c05fd9869
--- /dev/null
+++ b/keep-ui/components/navbar/AILink.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { Subtitle } from "@tremor/react";
+import { LinkWithIcon } from "components/LinkWithIcon";
+import { RiSparkling2Line } from "react-icons/ri";
+
+import { useEffect, useState } from "react";
+
+export const AILink = () => {
+  const [text, setText] = useState("");
+  const [newText, setNewText] = useState("AI correlation");
+
+
+  useEffect(() => {
+    let index = 0;
+
+    const interval = setInterval(() => {
+      setText(newText.slice(0, index + 1));
+      index++;
+
+      if (index === newText.length) {
+        clearInterval(interval);
+      }
+    }, 100);
+
+    return () => {
+      clearInterval(interval);
+    };
+  }, [newText]);
+
+  return (
+    <LinkWithIcon href="/ai" icon={RiSparkling2Line} className="w-full">
+      <div className="flex justify-between items-center w-full">
+        <Subtitle>
+          {text}
+        </Subtitle>
+      </div>
+    </LinkWithIcon>
+  );
+};
diff --git a/keep-ui/components/navbar/IncidentLinks.tsx b/keep-ui/components/navbar/IncidentLinks.tsx
new file mode 100644
index 0000000000..f6bc02ea77
--- /dev/null
+++ b/keep-ui/components/navbar/IncidentLinks.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { Subtitle } from "@tremor/react";
+import { LinkWithIcon } from "components/LinkWithIcon";
+import { DoorbellNotification} from "components/icons";
+import { Session } from "next-auth";
+import { Disclosure } from "@headlessui/react";
+import { IoChevronUp } from "react-icons/io5";
+import classNames from "classnames";
+
+type IncidentsLinksProps = { session: Session | null };
+
+export const IncidentsLinks = ({ session }: IncidentsLinksProps) => {
+  const isNOCRole = session?.userRole === "noc";
+
+  if (isNOCRole) {
+    return null;
+  }
+
+  return (
+    <Disclosure as="div" className="space-y-1" defaultOpen>
+      <Disclosure.Button className="w-full flex justify-between items-center p-2">
+        {({ open }) => (
+          <>
+            <Subtitle className="text-xs ml-2 text-gray-900 font-medium uppercase">
+              INCIDENTS
+            </Subtitle>
+            <IoChevronUp
+              className={classNames(
+                { "rotate-180": open },
+                "mr-2 text-slate-400"
+              )}
+            />
+          </>
+        )}
+      </Disclosure.Button>
+
+      <Disclosure.Panel as="ul" className="space-y-2 p-2 pr-4">
+          <li>
+            <LinkWithIcon
+              href="/incidents"
+              icon={DoorbellNotification}
+              isBeta={true}
+            >
+              <Subtitle>Incidents</Subtitle>
+            </LinkWithIcon>
+          </li>
+      </Disclosure.Panel>
+    </Disclosure>
+  );
+};
diff --git a/keep-ui/components/navbar/Navbar.tsx b/keep-ui/components/navbar/Navbar.tsx
index a394fb7fce..074d7d9567 100644
--- a/keep-ui/components/navbar/Navbar.tsx
+++ b/keep-ui/components/navbar/Navbar.tsx
@@ -8,6 +8,7 @@ import { Menu } from "components/navbar/Menu";
 import { MinimizeMenuButton } from "components/navbar/MinimizeMenuButton";
 import { authOptions } from "pages/api/auth/[...nextauth]";
 import { DashboardLinks } from "@/components/navbar/DashboardLinks";
+import { IncidentsLinks } from "@/components/navbar/IncidentLinks";
 
 export default async function NavbarInner() {
   const session = await getServerSession(authOptions);
@@ -19,6 +20,7 @@ export default async function NavbarInner() {
         <Search />
         <div className="pt-6 space-y-4 flex-1 overflow-auto">
           <AlertsLinks session={session} />
+          <IncidentsLinks session={session}/>
           <NoiseReductionLinks session={session} />
           <DashboardLinks session={session} />
         </div>
diff --git a/keep-ui/components/navbar/NoiseReductionLinks.tsx b/keep-ui/components/navbar/NoiseReductionLinks.tsx
index ad62f9a92e..d24f01eca1 100644
--- a/keep-ui/components/navbar/NoiseReductionLinks.tsx
+++ b/keep-ui/components/navbar/NoiseReductionLinks.tsx
@@ -7,6 +7,7 @@ import { Session } from "next-auth";
 import { Disclosure } from "@headlessui/react";
 import { IoChevronUp } from "react-icons/io5";
 import classNames from "classnames";
+import { AILink } from "./AILink";
 
 type NoiseReductionLinksProps = { session: Session | null };
 
@@ -64,6 +65,9 @@ export const NoiseReductionLinks = ({ session }: NoiseReductionLinksProps) => {
             </Subtitle>
           </LinkWithIcon>
         </li>
+        <li>
+         <AILink></AILink>
+        </li>
       </Disclosure.Panel>
     </Disclosure>
   );
diff --git a/keep-ui/public/grafana.svg b/keep-ui/public/grafana.svg
index 70100c6085..835f250201 100644
--- a/keep-ui/public/grafana.svg
+++ b/keep-ui/public/grafana.svg
@@ -142,7 +142,7 @@
 	.st2{fill:#67308F;}
 	.st3{fill:#D1D1D1;}
 	.st4{fill:#EF5B28;}
-	.st5{stroke:#000000;stroke-width:16;stroke-miterlimit:10;}
+	.st5{stroke:#000000;strokeWidth:16;stroke-miterlimit:10;}
 	.st6{fill:url(#Triangle-3_1_);}
 	.st7{fill:#333333;}
 	.st8{fill:#CF2129;}
diff --git a/keep-ui/public/incidentmanager.svg b/keep-ui/public/incidentmanager.svg
index df45ad1f67..2a2f83e536 100644
--- a/keep-ui/public/incidentmanager.svg
+++ b/keep-ui/public/incidentmanager.svg
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <title>Icon-Resource/Management-Governance/Res_AWS-Systems-Manager_Incident-Manager_48_Dark</title>
-    <g id="Icon-Resource/Management-Governance/Res_AWS-Systems-Manager_Incident-Manager_48" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="Icon-Resource/Management-Governance/Res_AWS-Systems-Manager_Incident-Manager_48" stroke="none" strokeWidth="1" fill="none" fill-rule="evenodd">
         <path d="M22.8408,15.3737048 L21.7798,13.6787048 C23.4628,12.6257048 25.3198,11.9467048 27.2988,11.6627048 L27.5838,13.6417048 C25.8818,13.8867048 24.2858,14.4697048 22.8408,15.3737048 L22.8408,15.3737048 Z M21.5358,16.3157048 C18.8628,18.5277048 17.2448,21.7727048 17.0988,25.2217048 L15.1008,25.1367048 C15.2718,21.1237048 17.1528,17.3467048 20.2608,14.7747048 L21.5358,16.3157048 Z M22.8088,22.8617048 C22.4078,23.8577048 22.2788,24.9507048 22.4328,26.0207048 L20.4518,26.3057048 C20.2478,24.8867048 20.4218,23.4377048 20.9528,22.1157048 L22.8088,22.8617048 Z M23.3808,21.7677048 L21.7108,20.6667048 C22.9918,18.7227048 25.0478,17.4167048 27.3528,17.0847048 L27.6378,19.0647048 C25.8998,19.3147048 24.3478,20.2997048 23.3808,21.7677048 L23.3808,21.7677048 Z M22.9998,30.9937048 L8.9558,30.9937048 C7.7818,30.9937048 6.9278,30.5497048 6.3138,30.1627048 C4.7038,29.1517048 1.9998,26.7847048 1.9998,22.3007048 C1.9998,16.9847048 5.5408,14.9527048 7.7268,14.2047048 L7.6948,13.6767048 C7.6938,9.20670483 10.6678,4.63570483 14.6128,2.98570483 C19.2208,1.04470483 24.1068,2.00470483 27.6778,5.56170483 C28.7238,6.60070483 29.5938,7.85270483 30.2748,9.29770483 C31.6918,8.24470483 33.5488,7.90970483 35.2748,8.48170483 C37.5208,9.22370483 38.9338,11.2547048 39.1468,13.9867048 C41.4608,14.5707048 45.9708,16.4697048 45.9708,22.3697048 L45.9718,22.6707048 C45.9741852,23.1217048 45.9768,23.8627048 45.9698,24.0187048 L43.9718,23.9307048 C43.9788,23.7847048 43.9738,23.0987048 43.9718,22.6807048 L43.9708,22.3697048 C43.9708,17.3537048 39.7868,16.0927048 37.9868,15.7827048 C37.7178,15.7367048 37.4788,15.5817048 37.3258,15.3537048 C37.1808,15.1357048 37.1258,14.8707048 37.1738,14.6147048 C37.1458,12.4427048 36.2288,10.9027048 34.6468,10.3807048 C33.2178,9.90670483 31.6368,10.4167048 30.7218,11.6507048 C30.5028,11.9477048 30.1338,12.0957048 29.7728,12.0447048 C29.4068,11.9907048 29.1028,11.7397048 28.9778,11.3927048 C28.3418,9.61970483 27.4298,8.13470483 26.2668,6.97970483 C24.8378,5.55670483 20.9168,2.49570483 15.3878,4.82970483 C12.1938,6.16570483 9.6938,10.0247048 9.6938,13.6167048 L9.7708,14.8767048 C9.7998,15.3567048 9.4838,15.7887048 9.0168,15.9067048 C7.1438,16.3787048 3.9998,17.8377048 3.9998,22.3007048 C3.9998,25.6457048 5.8378,27.5017048 7.3788,28.4697048 C7.9778,28.8477048 8.4188,28.9937048 8.9558,28.9937048 L22.9998,28.9937048 L22.9998,30.9937048 Z M35.7668,42.7787048 L37.7668,42.7787048 C37.7668,44.5407048 36.3328,45.9747048 34.5698,45.9747048 C32.8078,45.9747048 31.3738,44.5407048 31.3738,42.7787048 L33.3738,42.7787048 C33.3738,43.4377048 33.9108,43.9747048 34.5698,43.9747048 C35.2298,43.9747048 35.7668,43.4377048 35.7668,42.7787048 L35.7668,42.7787048 Z M43.9998,38.7727048 C43.9998,39.4457048 43.4518,39.9937048 42.7788,39.9937048 L26.2208,39.9937048 C25.5478,39.9937048 24.9998,39.4457048 24.9998,38.7727048 L24.9998,38.2147048 C24.9998,37.5417048 25.5478,36.9937048 26.2208,36.9937048 L42.7788,36.9937048 C43.4518,36.9937048 43.9998,37.5417048 43.9998,38.2147048 L43.9998,38.7727048 Z M27.9998,29.1547048 C27.9998,25.9357048 29.5028,23.6077048 32.0208,22.9297048 C32.4568,22.8117048 32.7608,22.4157048 32.7608,21.9637048 L32.7608,19.8787048 C32.7608,19.5327048 32.9608,19.2027048 33.3108,18.9757048 C34.0168,18.5167048 34.6428,18.7297048 34.9518,18.8977048 C35.4468,19.1657048 35.7148,19.6117048 35.7148,19.8947048 L35.7148,21.7627048 C35.7148,22.2297048 36.0378,22.6347048 36.4928,22.7377048 C39.2738,23.3697048 40.9998,25.8757048 40.9998,29.2797048 L40.9998,34.9937048 L27.9998,34.9937048 L27.9998,29.1547048 Z M42.9998,35.0157048 L42.9998,29.2797048 C42.9998,25.2047048 40.9958,22.1077048 37.7148,21.0057048 L37.7148,19.8947048 C37.7148,18.8177048 37.0048,17.7357048 35.9058,17.1397048 C34.7318,16.5027048 33.3528,16.5607048 32.2198,17.2987048 C31.3068,17.8937048 30.7608,18.8577048 30.7608,19.8787048 L30.7608,21.2387048 C27.7638,22.3827048 25.9998,25.2847048 25.9998,29.1547048 L25.9998,34.9937048 L26.2208,34.9937048 C24.4448,34.9937048 22.9998,36.4387048 22.9998,38.2147048 L22.9998,38.7727048 C22.9998,40.5487048 24.4448,41.9937048 26.2208,41.9937048 L42.7788,41.9937048 C44.5558,41.9937048 45.9998,40.5487048 45.9998,38.7727048 L45.9998,38.2147048 C45.9998,36.5147048 44.6718,35.1317048 42.9998,35.0157048 L42.9998,35.0157048 Z" id="Fill-1" fill="#FF4F8B"></path>
     </g>
-</svg>
\ No newline at end of file
+</svg>
diff --git a/keep-ui/public/psql.svg b/keep-ui/public/psql.svg
index 517562eb10..3b94ab70e4 100644
--- a/keep-ui/public/psql.svg
+++ b/keep-ui/public/psql.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60"><style><![CDATA[.B{stroke-width:.866}.C{stroke:#fff}.D{stroke-linecap:round}.E{stroke-linejoin:round}]]></style><path d="M13.56 43.927H8.34v-5.25h5.504c2.014 0 3.022.9 3.022 2.666 0 1.723-1.102 2.585-3.305 2.585m3.752-5.312c-.715-.64-1.716-.96-3.003-.96l-7.476.04v12.56H8.34l-.014-5.347h5.563c1.395 0 2.493-.32 3.294-.96s1.202-1.53 1.202-2.666-.358-2.026-1.073-2.666zm9.747 9.004a2.56 2.56 0 0 1-.715 1.068c-.675.66-1.704.987-3.1.987-1.25 0-2.2-.32-2.88-.96s-1.005-1.497-1.005-2.57c0-.56.088-1.055.263-1.488a2.92 2.92 0 0 1 .762-1.096c.692-.63 1.646-.947 2.86-.947 2.7 0 4.033 1.177 4.033 3.53 0 .56-.077 1.05-.23 1.474m.273-4.633c-.918-.837-2.277-1.256-4.077-1.256-1.7 0-3.015.428-3.942 1.283-.837.774-1.255 1.818-1.255 3.132 0 1.305.432 2.358 1.296 3.16.477.432 1.033.745 1.667.938s1.38.3 2.234.3c1.8 0 3.154-.414 4.1-1.242.837-.756 1.255-1.804 1.255-3.145 0-1.35-.423-2.403-1.27-3.16zm11.4 4.997c0 1.7-1.515 2.536-4.544 2.536-.926 0-1.706-.06-2.34-.176s-1.148-.313-1.544-.584a2.31 2.31 0 0 1-.728-.8c-.18-.323-.297-.75-.35-1.255l1.4-.013c0 .665.247 1.155.74 1.47.53.378 1.467.567 2.813.567 2.117 0 3.176-.53 3.176-1.592 0-.62-.355-1.047-1.065-1.28-.144-.045-1-.164-2.57-.357s-2.546-.367-2.933-.52c-.836-.333-1.254-.93-1.254-1.794a2.02 2.02 0 0 1 1.12-1.889c.37-.17.83-.303 1.384-.398s1.195-.142 1.924-.142c2.916 0 4.374.915 4.374 2.745h-1.34c0-1.286-1.025-1.93-3.075-1.93-1.043 0-1.82.13-2.333.4s-.77.66-.77 1.2c0 .504.346.868 1.038 1.093.405.117 1.335.252 2.792.405 1.304.117 2.198.283 2.684.5.926.36 1.4.97 1.4 1.834zm5.536 2.3l-.573.08-.358.027c-.918 0-1.58-.133-1.985-.4s-.6-.732-.6-1.4v-5.73h-1.67v-.843h1.66l.013-2.708h1.285v2.708h2.238v.857l-2.238-.014v5.577c0 .415.1.702.33.86s.614.237 1.18.237c.08.01.17.006.27-.01s.247-.05.445-.102l.014.86zm8.887-1.912a3.81 3.81 0 0 1-1.2.661c-.45.153-.966.23-1.55.23-2.688 0-4.03-1.1-4.03-3.33 0-1.033.344-1.842 1.03-2.426s1.656-.876 2.905-.876c1.258 0 2.213.294 2.865.883s.978 1.377.978 2.366c0 1.078-.333 1.9-.998 2.494m2.283-6.36h-1.285l-.014 1.417c-.566-.575-.97-.94-1.214-1.092-.72-.395-1.65-.593-2.792-.593-1.673 0-2.95.382-3.83 1.146-.836.7-1.254 1.717-1.254 3.047 0 .656.106 1.243.317 1.76s.52.955.925 1.314c.883.737 2.13 1.106 3.742 1.106 1.936 0 3.3-.53 4.12-1.6v1.467c0 .935-.295 1.66-.884 2.172s-1.558.77-2.907.77c-1.268 0-2.172-.175-2.7-.524-.405-.268-.684-.734-.836-1.397H45.5c.144.925.535 1.595 1.174 2.008.738.476 1.984.714 3.738.714 3.356 0 5.034-1.247 5.034-3.742v-7.983zm6.95.835a6.72 6.72 0 0 0-1.078-.094c-2.112 0-3.168 1.158-3.168 3.475v4.028h-1.27l-.014-8.243 1.285.013v1.47h.067c.216-.58.557-1.02 1.025-1.318.512-.3 1.276-.435 2.292-.435h.863v1.103zm1.196 2.766c.162-.937.526-1.662 1.092-2.176.37-.297.787-.52 1.254-.67s.994-.223 1.578-.223c1.142 0 2.074.313 2.798.94s1.095 1.336 1.095 2.13h-7.807m7.933-2.543a4.12 4.12 0 0 0-1.674-1.001c-.666-.217-1.444-.325-2.335-.325-1.665 0-2.956.387-3.874 1.16s-1.377 1.827-1.377 3.16.44 2.4 1.322 3.172c.953.864 2.298 1.296 4.035 1.296.692 0 1.333-.08 1.922-.235s1.104-.38 1.545-.665c.774-.556 1.263-1.24 1.47-2.056h-1.4c-.477 1.4-1.663 2.114-3.56 2.114-1.34 0-2.37-.344-3.1-1.034-.62-.6-.93-1.356-.93-2.296h9.16c0-1.398-.4-2.495-1.228-3.3z" fill="#231f20"/><path d="M86.32 46.888c0 2.447-1.974 3.67-5.92 3.67-2.365 0-4.092-.42-5.18-1.265-.9-.693-1.335-1.75-1.335-3.17h1.6c0 2.285 1.64 3.427 4.925 3.427 1.574 0 2.713-.234 3.414-.7.594-.413.9-1.06.9-1.94 0-.872-.405-1.483-1.215-1.834-.46-.2-1.58-.427-3.36-.715-2.24-.342-3.693-.683-4.358-1.024-.962-.503-1.444-1.357-1.444-2.562 0-1.123.4-1.96 1.228-2.508.88-.602 2.393-.903 4.534-.903 3.87 0 5.803 1.285 5.803 3.855H84.3c0-1.925-1.437-2.887-4.3-2.887-1.44 0-2.494.194-3.16.58-.63.414-.945 1.02-.945 1.822 0 .837.405 1.426 1.215 1.768.43.198 1.7.472 3.806.824 1.98.297 3.315.643 4 1.04.945.486 1.417 1.327 1.417 2.524zm12.908 1.25a6.2 6.2 0 0 1-2.012 1.071c-.748.238-1.585.357-2.512.357-1.9 0-3.42-.5-4.586-1.53s-1.75-2.377-1.75-4.074c0-1.77.578-3.147 1.735-4.135s2.7-1.482 4.6-1.482c1.936 0 3.458.516 4.565 1.55.585.503 1.017 1.1 1.297 1.765s.42 1.44.42 2.303c0 1.76-.586 3.152-1.756 4.176m.845.833c.828-.584 1.44-1.3 1.837-2.123s.595-1.8.595-2.878c0-1.95-.648-3.514-1.944-4.7-.693-.647-1.53-1.128-2.5-1.442s-2.1-.472-3.36-.472c-2.447 0-4.368.593-5.763 1.78s-2.092 2.795-2.092 4.826c0 1.977.703 3.572 2.1 4.785s3.33 1.82 5.768 1.82c.953 0 1.785-.065 2.496-.193s1.313-.327 1.808-.594l2.348 1.627.98-.9-2.273-1.542zm14.33 1.283h-10.858V37.68l1.493-.027v11.55h9.38l-.014 1.05z" fill="#367faf"/><path d="M67.415 27.556c.197-1.64.138-1.88 1.36-1.614l.3.027c.94.043 2.167-.15 2.89-.486 1.553-.72 2.474-1.924.943-1.608-3.494.72-3.734-.462-3.734-.462 3.69-5.474 5.23-12.423 3.9-14.123-3.63-4.64-9.916-2.445-10.02-2.388l-.033.006c-.69-.143-1.463-.23-2.33-.243-1.58-.026-2.78.414-3.7 1.105 0 0-11.212-4.62-10.7 5.81.11 2.218 3.18 16.786 6.84 12.386 1.338-1.61 2.63-2.97 2.63-2.97.642.427 1.4.644 2.217.566l.062-.053a2.43 2.43 0 0 0 .025.626c-.943 1.053-.666 1.24-2.55 1.627-1.907.393-.787 1.093-.055 1.276.887.222 2.94.536 4.325-1.405l-.055.222c.37.296.345 2.127.397 3.435s.14 2.53.407 3.25.58 2.574 3.06 2.043c2.07-.444 3.654-1.082 3.798-7.023" fill="#231f20" stroke="#231f20" stroke-width="2.598"/><path d="M72.916 23.875c-3.494.72-3.734-.462-3.734-.462 3.69-5.474 5.23-12.423 3.9-14.124-3.63-4.64-9.916-2.445-10.02-2.388l-.034.006c-.69-.143-1.463-.23-2.33-.243-1.58-.026-2.78.414-3.7 1.105 0 0-11.212-4.62-10.7 5.81.11 2.22 3.18 16.786 6.84 12.386 1.338-1.61 2.63-2.97 2.63-2.97.642.427 1.41.644 2.216.566l.063-.053c-.02.2-.01.395.025.626-.943 1.053-.666 1.24-2.55 1.627-1.907.393-.787 1.093-.055 1.276.887.222 2.94.536 4.325-1.405l-.055.222c.37.296.63 1.925.585 3.4s-.072 2.49.22 3.282.58 2.574 3.06 2.043c2.07-.444 3.144-1.594 3.293-3.512.106-1.364.346-1.162.36-2.38l.192-.577c.222-1.85.035-2.445 1.31-2.167l.3.027c.94.043 2.168-.15 2.89-.486 1.553-.72 2.474-1.924.943-1.608z" fill="#417ba2"/><g fill="none" class="C B"><g class="D"><path d="M59.96 24.935c-.096 3.44.024 6.903.36 7.745s1.057 2.48 3.536 1.948c2.07-.444 2.824-1.303 3.15-3.2l.765-6.063M57 7.692s-11.22-4.586-10.7 5.842c.1 2.22 3.18 16.787 6.84 12.387 1.338-1.6 2.547-2.87 2.547-2.87M63.05 6.855c-.4.122 6.24-2.423 10.008 2.4 1.33 1.7-.2 8.65-3.9 14.124" class="E"/><path d="M69.157 23.37s.24 1.183 3.735.462c1.53-.316.6.887-.943 1.608-1.274.59-4.13.743-4.178-.074-.12-2.11 1.504-1.468 1.386-1.996-.106-.476-.832-.943-1.312-2.107-.42-1.017-5.75-8.812 1.48-7.654.265-.055-1.886-6.877-8.652-6.988s-6.542 8.32-6.542 8.32" stroke-linejoin="bevel"/></g><g class="E"><path d="M58.066 24.09c-.943 1.053-.666 1.24-2.55 1.627-1.907.393-.787 1.093-.055 1.276.887.222 2.94.536 4.325-1.405.422-.59-.002-1.534-.582-1.775-.28-.116-.655-.26-1.136.277z"/><path d="M58.004 24.072c-.095-.62.204-1.356.523-2.22.48-1.294 1.6-2.588.703-6.692-.66-3.058-5.098-.637-5.1-.222s.2 2.102-.074 4.067c-.36 2.564 1.63 4.733 3.923 4.51" class="D"/></g></g><g fill="#fff" class="C"><path d="M56.948 14.877c-.02.142.26.52.623.57s.675-.245.694-.386-.26-.298-.624-.35-.675.023-.694.165z" stroke-width=".289"/><path d="M68.022 14.588c.02.142-.26.52-.623.57s-.675-.245-.695-.386.26-.298.624-.348.675.023.695.165z" stroke-width=".144"/></g><path d="M69.323 13.608c.06 1.11-.24 1.868-.277 3.05-.056 1.72.82 3.687-.5 5.657" fill="none" class="B C D E"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60"><style><![CDATA[.B{strokeWidth:.866}.C{stroke:#fff}.D{stroke-linecap:round}.E{stroke-linejoin:round}]]></style><path d="M13.56 43.927H8.34v-5.25h5.504c2.014 0 3.022.9 3.022 2.666 0 1.723-1.102 2.585-3.305 2.585m3.752-5.312c-.715-.64-1.716-.96-3.003-.96l-7.476.04v12.56H8.34l-.014-5.347h5.563c1.395 0 2.493-.32 3.294-.96s1.202-1.53 1.202-2.666-.358-2.026-1.073-2.666zm9.747 9.004a2.56 2.56 0 0 1-.715 1.068c-.675.66-1.704.987-3.1.987-1.25 0-2.2-.32-2.88-.96s-1.005-1.497-1.005-2.57c0-.56.088-1.055.263-1.488a2.92 2.92 0 0 1 .762-1.096c.692-.63 1.646-.947 2.86-.947 2.7 0 4.033 1.177 4.033 3.53 0 .56-.077 1.05-.23 1.474m.273-4.633c-.918-.837-2.277-1.256-4.077-1.256-1.7 0-3.015.428-3.942 1.283-.837.774-1.255 1.818-1.255 3.132 0 1.305.432 2.358 1.296 3.16.477.432 1.033.745 1.667.938s1.38.3 2.234.3c1.8 0 3.154-.414 4.1-1.242.837-.756 1.255-1.804 1.255-3.145 0-1.35-.423-2.403-1.27-3.16zm11.4 4.997c0 1.7-1.515 2.536-4.544 2.536-.926 0-1.706-.06-2.34-.176s-1.148-.313-1.544-.584a2.31 2.31 0 0 1-.728-.8c-.18-.323-.297-.75-.35-1.255l1.4-.013c0 .665.247 1.155.74 1.47.53.378 1.467.567 2.813.567 2.117 0 3.176-.53 3.176-1.592 0-.62-.355-1.047-1.065-1.28-.144-.045-1-.164-2.57-.357s-2.546-.367-2.933-.52c-.836-.333-1.254-.93-1.254-1.794a2.02 2.02 0 0 1 1.12-1.889c.37-.17.83-.303 1.384-.398s1.195-.142 1.924-.142c2.916 0 4.374.915 4.374 2.745h-1.34c0-1.286-1.025-1.93-3.075-1.93-1.043 0-1.82.13-2.333.4s-.77.66-.77 1.2c0 .504.346.868 1.038 1.093.405.117 1.335.252 2.792.405 1.304.117 2.198.283 2.684.5.926.36 1.4.97 1.4 1.834zm5.536 2.3l-.573.08-.358.027c-.918 0-1.58-.133-1.985-.4s-.6-.732-.6-1.4v-5.73h-1.67v-.843h1.66l.013-2.708h1.285v2.708h2.238v.857l-2.238-.014v5.577c0 .415.1.702.33.86s.614.237 1.18.237c.08.01.17.006.27-.01s.247-.05.445-.102l.014.86zm8.887-1.912a3.81 3.81 0 0 1-1.2.661c-.45.153-.966.23-1.55.23-2.688 0-4.03-1.1-4.03-3.33 0-1.033.344-1.842 1.03-2.426s1.656-.876 2.905-.876c1.258 0 2.213.294 2.865.883s.978 1.377.978 2.366c0 1.078-.333 1.9-.998 2.494m2.283-6.36h-1.285l-.014 1.417c-.566-.575-.97-.94-1.214-1.092-.72-.395-1.65-.593-2.792-.593-1.673 0-2.95.382-3.83 1.146-.836.7-1.254 1.717-1.254 3.047 0 .656.106 1.243.317 1.76s.52.955.925 1.314c.883.737 2.13 1.106 3.742 1.106 1.936 0 3.3-.53 4.12-1.6v1.467c0 .935-.295 1.66-.884 2.172s-1.558.77-2.907.77c-1.268 0-2.172-.175-2.7-.524-.405-.268-.684-.734-.836-1.397H45.5c.144.925.535 1.595 1.174 2.008.738.476 1.984.714 3.738.714 3.356 0 5.034-1.247 5.034-3.742v-7.983zm6.95.835a6.72 6.72 0 0 0-1.078-.094c-2.112 0-3.168 1.158-3.168 3.475v4.028h-1.27l-.014-8.243 1.285.013v1.47h.067c.216-.58.557-1.02 1.025-1.318.512-.3 1.276-.435 2.292-.435h.863v1.103zm1.196 2.766c.162-.937.526-1.662 1.092-2.176.37-.297.787-.52 1.254-.67s.994-.223 1.578-.223c1.142 0 2.074.313 2.798.94s1.095 1.336 1.095 2.13h-7.807m7.933-2.543a4.12 4.12 0 0 0-1.674-1.001c-.666-.217-1.444-.325-2.335-.325-1.665 0-2.956.387-3.874 1.16s-1.377 1.827-1.377 3.16.44 2.4 1.322 3.172c.953.864 2.298 1.296 4.035 1.296.692 0 1.333-.08 1.922-.235s1.104-.38 1.545-.665c.774-.556 1.263-1.24 1.47-2.056h-1.4c-.477 1.4-1.663 2.114-3.56 2.114-1.34 0-2.37-.344-3.1-1.034-.62-.6-.93-1.356-.93-2.296h9.16c0-1.398-.4-2.495-1.228-3.3z" fill="#231f20"/><path d="M86.32 46.888c0 2.447-1.974 3.67-5.92 3.67-2.365 0-4.092-.42-5.18-1.265-.9-.693-1.335-1.75-1.335-3.17h1.6c0 2.285 1.64 3.427 4.925 3.427 1.574 0 2.713-.234 3.414-.7.594-.413.9-1.06.9-1.94 0-.872-.405-1.483-1.215-1.834-.46-.2-1.58-.427-3.36-.715-2.24-.342-3.693-.683-4.358-1.024-.962-.503-1.444-1.357-1.444-2.562 0-1.123.4-1.96 1.228-2.508.88-.602 2.393-.903 4.534-.903 3.87 0 5.803 1.285 5.803 3.855H84.3c0-1.925-1.437-2.887-4.3-2.887-1.44 0-2.494.194-3.16.58-.63.414-.945 1.02-.945 1.822 0 .837.405 1.426 1.215 1.768.43.198 1.7.472 3.806.824 1.98.297 3.315.643 4 1.04.945.486 1.417 1.327 1.417 2.524zm12.908 1.25a6.2 6.2 0 0 1-2.012 1.071c-.748.238-1.585.357-2.512.357-1.9 0-3.42-.5-4.586-1.53s-1.75-2.377-1.75-4.074c0-1.77.578-3.147 1.735-4.135s2.7-1.482 4.6-1.482c1.936 0 3.458.516 4.565 1.55.585.503 1.017 1.1 1.297 1.765s.42 1.44.42 2.303c0 1.76-.586 3.152-1.756 4.176m.845.833c.828-.584 1.44-1.3 1.837-2.123s.595-1.8.595-2.878c0-1.95-.648-3.514-1.944-4.7-.693-.647-1.53-1.128-2.5-1.442s-2.1-.472-3.36-.472c-2.447 0-4.368.593-5.763 1.78s-2.092 2.795-2.092 4.826c0 1.977.703 3.572 2.1 4.785s3.33 1.82 5.768 1.82c.953 0 1.785-.065 2.496-.193s1.313-.327 1.808-.594l2.348 1.627.98-.9-2.273-1.542zm14.33 1.283h-10.858V37.68l1.493-.027v11.55h9.38l-.014 1.05z" fill="#367faf"/><path d="M67.415 27.556c.197-1.64.138-1.88 1.36-1.614l.3.027c.94.043 2.167-.15 2.89-.486 1.553-.72 2.474-1.924.943-1.608-3.494.72-3.734-.462-3.734-.462 3.69-5.474 5.23-12.423 3.9-14.123-3.63-4.64-9.916-2.445-10.02-2.388l-.033.006c-.69-.143-1.463-.23-2.33-.243-1.58-.026-2.78.414-3.7 1.105 0 0-11.212-4.62-10.7 5.81.11 2.218 3.18 16.786 6.84 12.386 1.338-1.61 2.63-2.97 2.63-2.97.642.427 1.4.644 2.217.566l.062-.053a2.43 2.43 0 0 0 .025.626c-.943 1.053-.666 1.24-2.55 1.627-1.907.393-.787 1.093-.055 1.276.887.222 2.94.536 4.325-1.405l-.055.222c.37.296.345 2.127.397 3.435s.14 2.53.407 3.25.58 2.574 3.06 2.043c2.07-.444 3.654-1.082 3.798-7.023" fill="#231f20" stroke="#231f20" strokeWidth="2.598"/><path d="M72.916 23.875c-3.494.72-3.734-.462-3.734-.462 3.69-5.474 5.23-12.423 3.9-14.124-3.63-4.64-9.916-2.445-10.02-2.388l-.034.006c-.69-.143-1.463-.23-2.33-.243-1.58-.026-2.78.414-3.7 1.105 0 0-11.212-4.62-10.7 5.81.11 2.22 3.18 16.786 6.84 12.386 1.338-1.61 2.63-2.97 2.63-2.97.642.427 1.41.644 2.216.566l.063-.053c-.02.2-.01.395.025.626-.943 1.053-.666 1.24-2.55 1.627-1.907.393-.787 1.093-.055 1.276.887.222 2.94.536 4.325-1.405l-.055.222c.37.296.63 1.925.585 3.4s-.072 2.49.22 3.282.58 2.574 3.06 2.043c2.07-.444 3.144-1.594 3.293-3.512.106-1.364.346-1.162.36-2.38l.192-.577c.222-1.85.035-2.445 1.31-2.167l.3.027c.94.043 2.168-.15 2.89-.486 1.553-.72 2.474-1.924.943-1.608z" fill="#417ba2"/><g fill="none" class="C B"><g class="D"><path d="M59.96 24.935c-.096 3.44.024 6.903.36 7.745s1.057 2.48 3.536 1.948c2.07-.444 2.824-1.303 3.15-3.2l.765-6.063M57 7.692s-11.22-4.586-10.7 5.842c.1 2.22 3.18 16.787 6.84 12.387 1.338-1.6 2.547-2.87 2.547-2.87M63.05 6.855c-.4.122 6.24-2.423 10.008 2.4 1.33 1.7-.2 8.65-3.9 14.124" class="E"/><path d="M69.157 23.37s.24 1.183 3.735.462c1.53-.316.6.887-.943 1.608-1.274.59-4.13.743-4.178-.074-.12-2.11 1.504-1.468 1.386-1.996-.106-.476-.832-.943-1.312-2.107-.42-1.017-5.75-8.812 1.48-7.654.265-.055-1.886-6.877-8.652-6.988s-6.542 8.32-6.542 8.32" stroke-linejoin="bevel"/></g><g class="E"><path d="M58.066 24.09c-.943 1.053-.666 1.24-2.55 1.627-1.907.393-.787 1.093-.055 1.276.887.222 2.94.536 4.325-1.405.422-.59-.002-1.534-.582-1.775-.28-.116-.655-.26-1.136.277z"/><path d="M58.004 24.072c-.095-.62.204-1.356.523-2.22.48-1.294 1.6-2.588.703-6.692-.66-3.058-5.098-.637-5.1-.222s.2 2.102-.074 4.067c-.36 2.564 1.63 4.733 3.923 4.51" class="D"/></g></g><g fill="#fff" class="C"><path d="M56.948 14.877c-.02.142.26.52.623.57s.675-.245.694-.386-.26-.298-.624-.35-.675.023-.694.165z" strokeWidth=".289"/><path d="M68.022 14.588c.02.142-.26.52-.623.57s-.675-.245-.695-.386.26-.298.624-.348.675.023.695.165z" strokeWidth=".144"/></g><path d="M69.323 13.608c.06 1.11-.24 1.868-.277 3.05-.056 1.72.82 3.687-.5 5.657" fill="none" class="B C D E"/></svg>
diff --git a/keep-ui/utils/hooks/useAIStats.ts b/keep-ui/utils/hooks/useAIStats.ts
new file mode 100644
index 0000000000..514786f42a
--- /dev/null
+++ b/keep-ui/utils/hooks/useAIStats.ts
@@ -0,0 +1,20 @@
+import { AIStats } from "app/ai/model";
+import { useSession } from "next-auth/react";
+import useSWR, { SWRConfiguration } from "swr";
+import { getApiURL } from "utils/apiUrl";
+import { fetcher } from "utils/fetcher";
+
+export const useAIStats = (
+  options: SWRConfiguration = {
+    revalidateOnFocus: false,
+  }
+) => {
+  const apiUrl = getApiURL();
+  const { data: session } = useSession();
+
+  return useSWR<AIStats>(
+    () => (session ? `${apiUrl}/ai/stats` : null),
+    (url) => fetcher(url, session?.accessToken),
+    options
+  );
+};
diff --git a/keep-ui/utils/hooks/useAlerts.ts b/keep-ui/utils/hooks/useAlerts.ts
index 0f5944a003..53b343b935 100644
--- a/keep-ui/utils/hooks/useAlerts.ts
+++ b/keep-ui/utils/hooks/useAlerts.ts
@@ -83,7 +83,7 @@ export const useAlerts = () => {
     options: SWRConfiguration = { revalidateOnFocus: false }
   ) => {
     return useSWR(
-      () => (session ? `${apiUrl}/alerts/${fingerprint}/audit` : null),
+      () => (session && fingerprint ? `${apiUrl}/alerts/${fingerprint}/audit` : null),
       (url) => fetcher(url, session?.accessToken),
       options
     );
diff --git a/keep-ui/utils/hooks/useIncidents.ts b/keep-ui/utils/hooks/useIncidents.ts
new file mode 100644
index 0000000000..722bd95dfd
--- /dev/null
+++ b/keep-ui/utils/hooks/useIncidents.ts
@@ -0,0 +1,97 @@
+import { AlertDto } from "app/alerts/models";
+import { IncidentDto, PaginatedIncidentsDto } from "app/incidents/model";
+import { useSession } from "next-auth/react";
+import useSWR, { SWRConfiguration } from "swr";
+import { getApiURL } from "utils/apiUrl";
+import { fetcher } from "utils/fetcher";
+import { useWebsocket } from "./usePusher";
+import { useCallback, useEffect } from "react";
+
+interface IncidentUpdatePayload {
+  incident_id: string | null;
+}
+
+export const useIncidents = (
+  confirmed: boolean = true,
+  limit: number = 25,
+  offset: number = 0,
+  options: SWRConfiguration = {
+    revalidateOnFocus: false,
+  }
+) => {
+  const apiUrl = getApiURL();
+  const { data: session } = useSession();
+  return useSWR<PaginatedIncidentsDto>(
+    () =>
+      session
+        ? `${apiUrl}/incidents?confirmed=${confirmed}&limit=${limit}&offset=${offset}`
+        : null,
+    (url) => fetcher(url, session?.accessToken),
+    options
+  );
+};
+
+export const useIncidentAlerts = (
+  incidentId: string,
+  options: SWRConfiguration = {
+    revalidateOnFocus: false,
+  }
+) => {
+  const apiUrl = getApiURL();
+  const { data: session } = useSession();
+  return useSWR<AlertDto[]>(
+    () => (session ? `${apiUrl}/incidents/${incidentId}/alerts` : null),
+    (url) => fetcher(url, session?.accessToken),
+    options
+  );
+};
+
+export const useIncident = (
+  incidentId: string,
+  options: SWRConfiguration = {
+    revalidateOnFocus: false,
+  }
+) => {
+  const apiUrl = getApiURL();
+  const { data: session } = useSession();
+
+  return useSWR<IncidentDto>(
+    () => (session ? `${apiUrl}/incidents/${incidentId}` : null),
+    (url) => fetcher(url, session?.accessToken),
+    options
+  );
+};
+
+export const usePollIncidentAlerts = (incidentId: string) => {
+  const { bind, unbind } = useWebsocket();
+  const { mutate } = useIncidentAlerts(incidentId);
+  const handleIncoming = useCallback(
+    (data: IncidentUpdatePayload) => {
+      mutate();
+    },
+    [mutate]
+  );
+  useEffect(() => {
+    bind("incident-change", handleIncoming);
+    return () => {
+      unbind("incident-change", handleIncoming);
+    };
+  }, [bind, unbind, handleIncoming]);
+};
+
+export const usePollIncidents = (mutateIncidents: any) => {
+  const { bind, unbind } = useWebsocket();
+  const handleIncoming = useCallback(
+    (data: any) => {
+      mutateIncidents();
+    },
+    [mutateIncidents]
+  );
+
+  useEffect(() => {
+    bind("incident-change", handleIncoming);
+    return () => {
+      unbind("incident-change", handleIncoming);
+    };
+  }, [bind, unbind, handleIncoming]);
+};
diff --git a/keep-ui/utils/hooks/usePresets.ts b/keep-ui/utils/hooks/usePresets.ts
index 7da5660660..a76989d896 100644
--- a/keep-ui/utils/hooks/usePresets.ts
+++ b/keep-ui/utils/hooks/usePresets.ts
@@ -2,24 +2,29 @@ import { useState, useEffect, useRef } from "react";
 import { Preset } from "app/alerts/models";
 import { useSession } from "next-auth/react";
 import useSWR, { SWRConfiguration } from "swr";
-import Pusher from "pusher-js";
 import { getApiURL } from "utils/apiUrl";
 import { fetcher } from "utils/fetcher";
 import { useLocalStorage } from "utils/hooks/useLocalStorage";
 import { useConfig } from "./useConfig";
 import useSWRSubscription from "swr/subscription";
+import { useWebsocket } from "./usePusher";
 
 export const usePresets = () => {
   const { data: session } = useSession();
   const { data: configData } = useConfig();
   const apiUrl = getApiURL();
 
-  const [presetsOrderFromLS, setPresetsOrderFromLS] = useLocalStorage<Preset[]>("presets-order", []);
-  const [staticPresetsOrderFromLS, setStaticPresetsOrderFromLS] = useLocalStorage<Preset[]>("static-presets-order", []);
+  const [presetsOrderFromLS, setPresetsOrderFromLS] = useLocalStorage<Preset[]>(
+    "presets-order",
+    []
+  );
+  const [staticPresetsOrderFromLS, setStaticPresetsOrderFromLS] =
+    useLocalStorage<Preset[]>("static-presets-order", []);
   // used to sync the presets with the server
   const [isLocalStorageReady, setIsLocalStorageReady] = useState(false);
   const presetsOrderRef = useRef(presetsOrderFromLS);
   const staticPresetsOrderRef = useRef(staticPresetsOrderFromLS);
+  const { bind, unbind } = useWebsocket();
 
   useEffect(() => {
     presetsOrderRef.current = presetsOrderFromLS;
@@ -28,8 +33,8 @@ export const usePresets = () => {
 
   const updateLocalPresets = (newPresets: Preset[]) => {
     const updatePresets = (currentPresets: Preset[], newPresets: Preset[]) => {
-      const newPresetMap = new Map(newPresets.map(p => [p.id, p]));
-      let updatedPresets = new Map(currentPresets.map(p => [p.id, p]));
+      const newPresetMap = new Map(newPresets.map((p) => [p.id, p]));
+      let updatedPresets = new Map(currentPresets.map((p) => [p.id, p]));
 
       newPresetMap.forEach((newPreset, newPresetId) => {
         const currentPreset = updatedPresets.get(newPresetId);
@@ -37,13 +42,13 @@ export const usePresets = () => {
           // Update existing preset with new alerts count
           updatedPresets.set(newPresetId, {
             ...currentPreset,
-            alerts_count: currentPreset.alerts_count + newPreset.alerts_count
+            alerts_count: currentPreset.alerts_count + newPreset.alerts_count,
           });
         } else {
           // If the preset is not in the current presets, add it
           updatedPresets.set(newPresetId, {
             ...newPreset,
-            alerts_count: newPreset.alerts_count
+            alerts_count: newPreset.alerts_count,
           });
         }
       });
@@ -51,85 +56,102 @@ export const usePresets = () => {
       return Array.from(updatedPresets.values());
     };
 
-    setPresetsOrderFromLS(current => updatePresets(presetsOrderRef.current, newPresets.filter(p => !['feed', 'deleted', 'dismissed', 'groups'].includes(p.name))));
-    setStaticPresetsOrderFromLS(current => updatePresets(staticPresetsOrderRef.current, newPresets.filter(p => ['feed', 'deleted', 'dismissed', 'groups'].includes(p.name))));
+    setPresetsOrderFromLS((current) =>
+      updatePresets(
+        presetsOrderRef.current,
+        newPresets.filter(
+          (p) => !["feed", "deleted", "dismissed", "groups"].includes(p.name)
+        )
+      )
+    );
+    setStaticPresetsOrderFromLS((current) =>
+      updatePresets(
+        staticPresetsOrderRef.current,
+        newPresets.filter((p) =>
+          ["feed", "deleted", "dismissed", "groups"].includes(p.name)
+        )
+      )
+    );
   };
 
   useSWRSubscription(
-    () => (configData?.PUSHER_DISABLED === false && session && isLocalStorageReady) ? "presets" : null,
+    () =>
+      configData?.PUSHER_DISABLED === false && session && isLocalStorageReady
+        ? "presets"
+        : null,
     (_, { next }) => {
-      console.log("Subscribing to presets channel")
-      const pusher = new Pusher(configData!.PUSHER_APP_KEY, {
-        wsHost: configData!.PUSHER_HOST,
-        wsPort: configData!.PUSHER_PORT,
-        forceTLS: false,
-        disableStats: true,
-        enabledTransports: ["ws", "wss"],
-        cluster: configData!.PUSHER_CLUSTER || "local",
-        channelAuthorization: {
-          transport: "ajax",
-          endpoint: `${apiUrl}/pusher/auth`,
-          headers: {
-            Authorization: `Bearer ${session!.accessToken}`
-          },
-        },
-      });
-
-      const channelName = `private-${session!.tenantId}`;
-      const channel = pusher.subscribe(channelName);
-
-      channel.bind("async-presets", (newPresets: Preset[]) => {
-        console.log("Received new presets from server", newPresets);
+      const newPresets = (newPresets: Preset[]) => {
         updateLocalPresets(newPresets);
         next(null, {
           presets: newPresets,
           isAsyncLoading: false,
           lastSubscribedDate: new Date(),
-          pusherChannel: channel
         });
-      });
+      };
+
+      bind("async-presets", newPresets);
 
       return () => {
-        console.log("Unsubscribing from presets channel")
-        pusher.unsubscribe(channelName);
+        console.log("Unbinding from presets channel");
+        unbind("async-presets", newPresets);
       };
     },
     { revalidateOnFocus: false }
   );
 
-
   const useFetchAllPresets = (options?: SWRConfiguration) => {
     return useSWR<Preset[]>(
       () => (session ? `${apiUrl}/preset` : null),
-      url => fetcher(url, session?.accessToken),
+      (url) => fetcher(url, session?.accessToken),
       {
         ...options,
         onSuccess: (data) => {
           if (data) {
-            const dynamicPresets = data.filter(p => !['feed', 'deleted', 'dismissed', 'groups'].includes(p.name));
-            const staticPresets = data.filter(p => ['feed', 'deleted', 'dismissed', 'groups'].includes(p.name));
-            mergePresetsWithLocalStorage(dynamicPresets, presetsOrderFromLS, setPresetsOrderFromLS);
-            mergePresetsWithLocalStorage(staticPresets, staticPresetsOrderFromLS, setStaticPresetsOrderFromLS);
+            const dynamicPresets = data.filter(
+              (p) =>
+                !["feed", "deleted", "dismissed", "groups"].includes(p.name)
+            );
+            const staticPresets = data.filter((p) =>
+              ["feed", "deleted", "dismissed", "groups"].includes(p.name)
+            );
+            mergePresetsWithLocalStorage(
+              dynamicPresets,
+              presetsOrderFromLS,
+              setPresetsOrderFromLS
+            );
+            mergePresetsWithLocalStorage(
+              staticPresets,
+              staticPresetsOrderFromLS,
+              setStaticPresetsOrderFromLS
+            );
           }
-        }
+        },
       }
     );
   };
 
-  const mergePresetsWithLocalStorage = (serverPresets: Preset[], localPresets: Preset[], setter: (presets: Preset[]) => void) => {
+  const mergePresetsWithLocalStorage = (
+    serverPresets: Preset[],
+    localPresets: Preset[],
+    setter: (presets: Preset[]) => void
+  ) => {
     // This map quickly checks presence by ID
-    const serverPresetIds = new Set(serverPresets.map(sp => sp.id));
+    const serverPresetIds = new Set(serverPresets.map((sp) => sp.id));
 
     // Filter localPresets to remove those not present in serverPresets
-    const updatedLocalPresets = localPresets.filter(lp => serverPresetIds.has(lp.id)).map(lp => {
+    const updatedLocalPresets = localPresets
+      .filter((lp) => serverPresetIds.has(lp.id))
+      .map((lp) => {
         // Find the server version of this local preset
-        const serverPreset = serverPresets.find(sp => sp.id === lp.id);
+        const serverPreset = serverPresets.find((sp) => sp.id === lp.id);
         // If found, merge, otherwise just return local (though filtered above)
-        return serverPreset ? {...lp, ...serverPreset} : lp;
-    });
+        return serverPreset ? { ...lp, ...serverPreset } : lp;
+      });
 
     // Filter serverPresets to find those not in local storage, to add new presets from server
-    const newServerPresets = serverPresets.filter(sp => !localPresets.some(lp => lp.id === sp.id));
+    const newServerPresets = serverPresets.filter(
+      (sp) => !localPresets.some((lp) => lp.id === sp.id)
+    );
 
     // Combine the updated local presets with any new server presets
     const combinedPresets = updatedLocalPresets.concat(newServerPresets);
@@ -137,12 +159,19 @@ export const usePresets = () => {
     // Update state with combined list
     setter(combinedPresets);
     setIsLocalStorageReady(true);
-};
-
+  };
 
   const useAllPresets = (options?: SWRConfiguration) => {
-    const { data: presets, error, isValidating, mutate } = useFetchAllPresets(options);
-    const filteredPresets = presets?.filter(preset => !['feed', 'deleted', 'dismissed', 'groups'].includes(preset.name));
+    const {
+      data: presets,
+      error,
+      isValidating,
+      mutate,
+    } = useFetchAllPresets(options);
+    const filteredPresets = presets?.filter(
+      (preset) =>
+        !["feed", "deleted", "dismissed", "groups"].includes(preset.name)
+    );
     return {
       data: filteredPresets,
       error,
@@ -152,8 +181,15 @@ export const usePresets = () => {
   };
 
   const useStaticPresets = (options?: SWRConfiguration) => {
-    const { data: presets, error, isValidating, mutate } = useFetchAllPresets(options);
-    const staticPresets = presets?.filter(preset => ['feed', 'deleted', 'dismissed', 'groups'].includes(preset.name));
+    const {
+      data: presets,
+      error,
+      isValidating,
+      mutate,
+    } = useFetchAllPresets(options);
+    const staticPresets = presets?.filter((preset) =>
+      ["feed", "deleted", "dismissed", "groups"].includes(preset.name)
+    );
     return {
       data: staticPresets,
       error,
@@ -162,5 +198,11 @@ export const usePresets = () => {
     };
   };
 
-  return { useAllPresets, useStaticPresets, presetsOrderFromLS, setPresetsOrderFromLS, staticPresetsOrderFromLS };
+  return {
+    useAllPresets,
+    useStaticPresets,
+    presetsOrderFromLS,
+    setPresetsOrderFromLS,
+    staticPresetsOrderFromLS,
+  };
 };
diff --git a/keep-ui/utils/hooks/usePusher.ts b/keep-ui/utils/hooks/usePusher.ts
index ba5c99ef55..0b57dd71f7 100644
--- a/keep-ui/utils/hooks/usePusher.ts
+++ b/keep-ui/utils/hooks/usePusher.ts
@@ -1,120 +1,100 @@
-import useSWRSubscription, { SWRSubscriptionOptions } from "swr/subscription";
-import Pusher, { Channel } from "pusher-js";
+import Pusher from "pusher-js";
 import { useConfig } from "./useConfig";
 import { useSession } from "next-auth/react";
 import { getApiURL } from "utils/apiUrl";
+import { useCallback, useEffect, useRef, useState } from "react";
 
-type PusherSubscription = {
-  pusherChannel: Channel | null;
-  pollAlerts: number;
-};
-
-export const getDefaultSubscriptionObj = (
-  pusherChannel: Channel | null = null
-): PusherSubscription => ({
-  pollAlerts: 0,
-  pusherChannel,
-});
+let PUSHER: Pusher | null = null;
+const POLLING_INTERVAL = 3000;
 
-export const usePusher = () => {
+export const useWebsocket = () => {
   const apiUrl = getApiURL();
   const { data: configData } = useConfig();
   const { data: session } = useSession();
-
-  return useSWRSubscription(
-    () => (configData?.PUSHER_DISABLED === false && session ? "alerts" : null),
-    (_, { next }: SWRSubscriptionOptions<PusherSubscription, Error>) => {
-      if (configData === undefined || session === null) {
-        console.log("Pusher disabled");
-
-        return () =>
-          next(null, {
-            pollAlerts: 0,
-            pusherChannel: null,
-          });
-      }
-
-      console.log("Connecting to pusher");
-      const pusher = new Pusher(configData.PUSHER_APP_KEY, {
-        wsHost: configData.PUSHER_HOST,
-        wsPort: configData.PUSHER_PORT,
-        forceTLS: false,
-        disableStats: true,
-        enabledTransports: ["ws", "wss"],
-        cluster: configData.PUSHER_CLUSTER || "local",
-        channelAuthorization: {
-          transport: "ajax",
-          endpoint: `${apiUrl}/pusher/auth`,
-          headers: {
-            Authorization: `Bearer ${session.accessToken!}`,
-          },
+  let channelName = `private-${session?.tenantId}`;
+
+  if (
+    PUSHER === null &&
+    configData !== undefined &&
+    session !== undefined &&
+    configData.PUSHER_DISABLED === false
+  ) {
+    channelName = `private-${session?.tenantId}`;
+    PUSHER = new Pusher(configData.PUSHER_APP_KEY, {
+      wsHost: configData.PUSHER_HOST,
+      wsPort: configData.PUSHER_PORT,
+      forceTLS: false,
+      disableStats: true,
+      enabledTransports: ["ws", "wss"],
+      cluster: configData.PUSHER_CLUSTER || "local",
+      channelAuthorization: {
+        transport: "ajax",
+        endpoint: `${apiUrl}/pusher/auth`,
+        headers: {
+          Authorization: `Bearer ${session?.accessToken!}`,
         },
-      });
-
-      const channelName = `private-${session.tenantId}`;
-      const pusherChannel = pusher.subscribe(channelName);
-
-      let lastPollTime = 0;
-
-
-      pusherChannel.bind("poll-alerts", (incoming: any) => {
-        const currentTime = Date.now();
-        const timeSinceLastPoll = currentTime - lastPollTime;
-
-        next(null, (data) => {
-          if (timeSinceLastPoll < 3000) {
-            // If less than 3 seconds since last poll, return pollAlerts: 0
-            if(data){
-              return {
-                ...(data),
-                pollAlerts: 0,
-              };
-             return {
-                pollAlerts: 0,
-                isAsyncLoading: false,
-                pusherChannel,
-              }
-            }
-
-          }
-          lastPollTime = currentTime;
-
-          if (data) {
-            return {
-              ...data,
-              pollAlerts: Math.floor(Math.random() * 10000),
-            };
-          }
-
-          return {
-            pollAlerts: Math.floor(Math.random() * 10000),
-            isAsyncLoading: false,
-            pusherChannel,
-          };
-        });
-      });
-
-      setTimeout(() => {
-        next(null, (data) => {
-          if (data) {
-            return { ...data };
-          }
-
-          return {
-            pollAlerts: 0,
-            pusherChannel,
-          };
-        });
-      }, 3500);
-
-      next(null, {
-        pollAlerts: 0,
-        pusherChannel,
-      });
-      console.log("Connected to pusher");
+      },
+    });
+    PUSHER.subscribe(channelName);
+  }
+
+  const subscribe = () => {
+    return PUSHER?.subscribe(channelName);
+  };
+
+  const unsubscribe = () => {
+    return PUSHER?.unsubscribe(channelName);
+  };
+
+  const bind = (event: any, callback: any) => {
+    return PUSHER?.channel(channelName)?.bind(event, callback);
+  };
+
+  const unbind = (event: any, callback: any) => {
+    return PUSHER?.channel(channelName)?.unbind(event, callback);
+  };
+
+  const trigger = (event: any, data: any) => {
+    return PUSHER?.channel(channelName).trigger(event, data);
+  };
+
+  const channel = () => {
+    return PUSHER?.channel(channelName);
+  };
+
+  return {
+    subscribe,
+    unsubscribe,
+    bind,
+    unbind,
+    trigger,
+    channel,
+  };
+};
 
-      return () => pusher.unsubscribe(channelName);
-    },
-    { revalidateOnFocus: false }
-  );
+export const useAlertPolling = () => {
+  const { bind, unbind } = useWebsocket();
+  const [pollAlerts, setPollAlerts] = useState(0);
+  const lastPollTimeRef = useRef(0);
+
+  const handleIncoming = useCallback((incoming: any) => {
+    const currentTime = Date.now();
+    const timeSinceLastPoll = currentTime - lastPollTimeRef.current;
+
+    if (timeSinceLastPoll < POLLING_INTERVAL) {
+      setPollAlerts(0);
+    } else {
+      lastPollTimeRef.current = currentTime;
+      setPollAlerts(Math.floor(Math.random() * 10000));
+    }
+  }, []);
+
+  useEffect(() => {
+    bind("poll-alerts", handleIncoming);
+    return () => {
+      unbind("poll-alerts", handleIncoming);
+    };
+  }, [bind, unbind, handleIncoming]);
+
+  return { data: pollAlerts };
 };
diff --git a/keep/api/api.py b/keep/api/api.py
index 469b199d89..6600c7c6c3 100644
--- a/keep/api/api.py
+++ b/keep/api/api.py
@@ -29,6 +29,7 @@
     extraction,
     groups,
     healthcheck,
+    incidents,
     mapping,
     preset,
     providers,
@@ -39,6 +40,8 @@
     users,
     whoami,
     workflows,
+    incidents,
+    ai
 )
 from keep.event_subscriber.event_subscriber import EventSubscriber
 from keep.posthog.posthog import get_posthog_client
@@ -160,6 +163,8 @@ def get_app(
     app.include_router(actions.router, prefix="/actions", tags=["actions"])
     app.include_router(healthcheck.router, prefix="/healthcheck", tags=["healthcheck"])
     app.include_router(alerts.router, prefix="/alerts", tags=["alerts"])
+    app.include_router(incidents.router, prefix="/incidents", tags=["incidents"])
+    app.include_router(ai.router, prefix="/ai", tags=["ai"])
     app.include_router(settings.router, prefix="/settings", tags=["settings"])
     app.include_router(
         workflows.router, prefix="/workflows", tags=["workflows", "alerts"]
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 6260c723ca..5e404f13f4 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -21,12 +21,13 @@
 from sqlalchemy.orm import joinedload, selectinload, subqueryload
 from sqlalchemy.orm.attributes import flag_modified
 from sqlalchemy.orm.exc import StaleDataError
-from sqlmodel import Session, or_, select
+from sqlalchemy.sql import expression
+from sqlmodel import Session, col, or_, select
 
 from keep.api.core.db_utils import create_db_engine
 
 # This import is required to create the tables
-from keep.api.models.alert import AlertStatus
+from keep.api.models.alert import AlertStatus, IncidentDtoIn
 from keep.api.models.db.action import Action
 from keep.api.models.db.alert import *  # pylint: disable=unused-wildcard-import
 from keep.api.models.db.dashboard import *  # pylint: disable=unused-wildcard-import
@@ -1758,6 +1759,40 @@ def update_preset_options(tenant_id: str, preset_id: str, options: dict) -> Pres
     return preset
 
 
+def get_incident_by_id(incident_id: UUID) -> Incident:
+    with Session(engine) as session:
+        incident = session.exec(
+            select(Incident)
+            .options(selectinload(Incident.alerts))
+            .where(Incident.id == incident_id)
+        ).first()
+    return incident
+
+
+def assign_alert_to_incident(
+    alert_id: UUID, incident_id: UUID, tenant_id: str
+) -> AlertToIncident:
+    with Session(engine) as session:
+        assignment = AlertToIncident(
+            alert_id=alert_id, incident_id=incident_id, tenant_id=tenant_id
+        )
+        session.add(assignment)
+        session.commit()
+        session.refresh(assignment)
+
+    return assignment
+
+
+def get_incidents(tenant_id) -> List[Incident]:
+    with Session(engine) as session:
+        incidents = session.exec(
+            select(Incident)
+            .options(selectinload(Incident.alerts))
+            .where(Incident.tenant_id == tenant_id)
+        ).all()
+    return incidents
+
+
 def get_alert_audit(
     tenant_id: str, fingerprint: str, limit: int = 50
 ) -> List[AlertAudit]:
@@ -1770,3 +1805,298 @@ def get_alert_audit(
             .limit(limit)
         ).all()
     return audit
+
+
+def get_last_incidents(
+    tenant_id: str,
+    limit: int = 25,
+    offset: int = 0,
+    timeframe: int = None,
+    is_confirmed: bool = False,
+) -> (list[Incident], int):
+    """
+    Get the last incidents and total amount of incidents.
+
+    Args:
+        tenant_id (str): The tenant_id to filter the incidents by.
+        limit (int): Amount of objects to return
+        offset (int): Current offset for
+        timeframe (int|null): Return incidents only for the last <N> days
+        is_confirmed (bool): Return confirmed incidents or predictions
+
+    Returns:
+        List[Incident]: A list of Incident objects.
+    """
+    with Session(engine) as session:
+        query = (
+            session.query(
+                Incident,
+            )
+            .filter(Incident.tenant_id == tenant_id)
+            .filter(Incident.is_confirmed == is_confirmed)
+            .options(joinedload(Incident.alerts))
+        )
+
+        if timeframe:
+            query = query.filter(
+                Incident.start_time
+                >= datetime.now(tz=timezone.utc) - timedelta(days=timeframe)
+            )
+
+        total_count = query.count()
+
+        # Order by timestamp in descending order and limit the results
+        query = query.order_by(desc(Incident.start_time)).limit(limit).offset(offset)
+        # Execute the query
+        incidents = query.all()
+
+    return incidents, total_count
+
+
+def get_incident_by_id(tenant_id: str, incident_id: str) -> Optional[Incident]:
+    with Session(engine) as session:
+        query = session.query(
+            Incident,
+        ).filter(
+            Incident.tenant_id == tenant_id,
+            Incident.id == incident_id,
+        )
+
+    return query.first()
+
+
+def create_incident_from_dto(
+    tenant_id: str, incident_dto: IncidentDtoIn
+) -> Optional[Incident]:
+    return create_incident_from_dict(tenant_id, incident_dto.dict())
+
+
+def create_incident_from_dict(
+    tenant_id: str, incident_data: dict
+) -> Optional[Incident]:
+    is_predicted = incident_data.get("is_predicted", False)
+    with Session(engine) as session:
+        new_incident = Incident(
+            **incident_data,
+            tenant_id=tenant_id,
+            is_confirmed=not is_predicted
+        )
+        session.add(new_incident)
+        session.commit()
+        session.refresh(new_incident)
+        new_incident.alerts = []
+    return new_incident
+
+
+def update_incident_from_dto_by_id(
+    tenant_id: str,
+    incident_id: str,
+    updated_incident_dto: IncidentDtoIn,
+) -> Optional[Incident]:
+    with Session(engine) as session:
+        incident = session.exec(
+            select(Incident).where(
+                Incident.tenant_id == tenant_id,
+                Incident.id == incident_id,
+            ).options(joinedload(Incident.alerts))
+        ).first()
+
+        if not incident:
+            return None
+
+        session.query(Incident).filter(
+            Incident.tenant_id == tenant_id,
+            Incident.id == incident_id,
+        ).update({
+            "name": updated_incident_dto.name,
+            "description": updated_incident_dto.description,
+            "assignee": updated_incident_dto.assignee,
+        })
+
+        session.commit()
+        session.refresh(incident)
+
+        return incident
+
+
+def delete_incident_by_id(
+    tenant_id: str,
+    incident_id: str,
+) -> bool:
+    with Session(engine) as session:
+        incident = (
+            session.query(Incident)
+            .filter(
+                Incident.tenant_id == tenant_id,
+                Incident.id == incident_id,
+            )
+            .first()
+        )
+
+        # Delete all associations with alerts:
+
+        (
+            session.query(AlertToIncident)
+            .where(
+                AlertToIncident.tenant_id == tenant_id,
+                AlertToIncident.incident_id == incident.id,
+            )
+            .delete()
+        )
+
+        session.delete(incident)
+        session.commit()
+        return True
+
+
+def get_incidents_count(
+    tenant_id: str,
+) -> int:
+    with Session(engine) as session:
+        return (
+            session.query(Incident)
+            .filter(
+                Incident.tenant_id == tenant_id,
+            )
+            .count()
+        )
+
+
+def get_incident_alerts_by_incident_id(tenant_id: str, incident_id: str) -> List[Alert]:
+    with Session(engine) as session:
+        query = (
+            session.query(
+                Alert,
+            )
+            .join(AlertToIncident, AlertToIncident.alert_id == Alert.id)
+            .join(Incident, AlertToIncident.incident_id == Incident.id)
+            .filter(
+                AlertToIncident.tenant_id == tenant_id,
+                Incident.id == incident_id,
+            )
+        )
+
+    return query.all()
+
+
+def add_alerts_to_incident_by_incident_id(
+    tenant_id: str, incident_id: str, alert_ids: List[UUID]
+):
+    with Session(engine) as session:
+        incident = session.exec(
+            select(Incident).where(
+                Incident.tenant_id == tenant_id,
+                Incident.id == incident_id,
+            )
+        ).first()
+
+        if not incident:
+            return None
+
+        existed_alert_ids = session.exec(
+            select(AlertToIncident.alert_id).where(
+                AlertToIncident.tenant_id == tenant_id,
+                AlertToIncident.incident_id == incident.id,
+                col(AlertToIncident.alert_id).in_(alert_ids),
+            )
+        ).all()
+
+        alert_to_incident_entries = [
+            AlertToIncident(
+                alert_id=alert_id, incident_id=incident.id, tenant_id=tenant_id
+            )
+            for alert_id in alert_ids
+            if alert_id not in existed_alert_ids
+        ]
+
+        session.bulk_save_objects(alert_to_incident_entries)
+        session.commit()
+        return True
+
+
+def remove_alerts_to_incident_by_incident_id(
+    tenant_id: str, incident_id: str, alert_ids: List[UUID]
+) -> Optional[int]:
+    with Session(engine) as session:
+        incident = session.exec(
+            select(Incident).where(
+                Incident.tenant_id == tenant_id,
+                Incident.id == incident_id,
+            )
+        ).first()
+
+        if not incident:
+            return None
+
+        deleted = (
+            session.query(AlertToIncident)
+            .where(
+                AlertToIncident.tenant_id == tenant_id,
+                AlertToIncident.incident_id == incident.id,
+                col(AlertToIncident.alert_id).in_(alert_ids),
+            )
+            .delete()
+        )
+
+        session.commit()
+        return deleted
+
+
+def get_alerts_count(
+    tenant_id: str,
+) -> int:
+    with Session(engine) as session:
+        return (
+            session.query(Alert)
+            .filter(
+                Alert.tenant_id == tenant_id,
+            )
+            .count()
+        )
+
+
+def get_first_alert_datetime(
+    tenant_id: str,
+) -> datetime | None:
+    with Session(engine) as session:
+        first_alert = (
+            session.query(Alert)
+            .filter(
+                Alert.tenant_id == tenant_id,
+            )
+            .first()
+        )
+        if first_alert:
+            return first_alert.timestamp
+
+
+def confirm_predicted_incident_by_id(
+    tenant_id: str,
+    incident_id: UUID | str,
+):
+    with Session(engine) as session:
+        incident = session.exec(
+            select(Incident).where(
+                Incident.tenant_id == tenant_id,
+                Incident.id == incident_id,
+                Incident.is_confirmed == expression.false()
+            ).options(
+                joinedload(Incident.alerts)
+            )
+        ).first()
+
+        if not incident:
+            return None
+
+        session.query(Incident).filter(
+            Incident.tenant_id == tenant_id,
+            Incident.id == incident_id,
+            Incident.is_confirmed == expression.false()
+        ).update({
+            "is_confirmed": True,
+        })
+
+        session.commit()
+        session.refresh(incident)
+
+        return incident
diff --git a/keep/api/models/alert.py b/keep/api/models/alert.py
index 17ce8b729f..c1cd7027b4 100644
--- a/keep/api/models/alert.py
+++ b/keep/api/models/alert.py
@@ -5,6 +5,7 @@
 import uuid
 from enum import Enum
 from typing import Any, Dict
+from uuid import UUID
 
 import pytz
 from pydantic import AnyHttpUrl, BaseModel, Extra, root_validator, validator
@@ -12,6 +13,21 @@
 logger = logging.getLogger(__name__)
 
 
+def get_fingerprint(fingerprint, values):
+    # if its none, use the name
+    if fingerprint is None:
+        fingerprint_payload = values.get("name")
+        # if the alert name is None, than use the entire payload
+        if not fingerprint_payload:
+            logger.warning("No name to alert, using the entire payload")
+            fingerprint_payload = json.dumps(values)
+        fingerprint = hashlib.sha256(fingerprint_payload.encode()).hexdigest()
+    # take only the first 255 characters
+    else:
+        fingerprint = fingerprint[:255]
+    return fingerprint
+
+
 class AlertSeverity(Enum):
     CRITICAL = ("critical", 5)
     HIGH = ("high", 4)
@@ -73,6 +89,14 @@ class AlertStatus(Enum):
     PENDING = "pending"
 
 
+class IncidentSeverity(Enum):
+    CRITICAL = "critical"
+    HIGH = "high"
+    MEDIUM = "medium"
+    LOW = "low"
+    INFO = "info"
+
+
 class AlertDto(BaseModel):
     id: str | None
     name: str
@@ -139,18 +163,7 @@ def __ne__(self, other):
 
     @validator("fingerprint", pre=True, always=True)
     def assign_fingerprint_if_none(cls, fingerprint, values):
-        # if its none, use the name
-        if fingerprint is None:
-            fingerprint_payload = values.get("name")
-            # if the alert name is None, than use the entire payload
-            if not fingerprint_payload:
-                logger.warning("No name to alert, using the entire payload")
-                fingerprint_payload = json.dumps(values)
-            fingerprint = hashlib.sha256(fingerprint_payload.encode()).hexdigest()
-        # take only the first 255 characters
-        else:
-            fingerprint = fingerprint[:255]
-        return fingerprint
+        return get_fingerprint(fingerprint, values)
 
     @validator("deleted", pre=True, always=True)
     def validate_deleted(cls, deleted, values):
@@ -310,3 +323,76 @@ class DismissRequestBody(BaseModel):
 class EnrichAlertRequestBody(BaseModel):
     enrichments: dict[str, str]
     fingerprint: str
+
+
+class IncidentDtoIn(BaseModel):
+    name: str
+    description: str
+    assignee: str | None
+
+    class Config:
+        extra = Extra.allow
+        schema_extra = {
+            "examples": [
+                {
+                    "id": "c2509cb3-6168-4347-b83b-a41da9df2d5b",
+                    "name": "Incident name",
+                    "description": "Keep: Incident description",
+                }
+            ]
+        }
+
+
+class IncidentDto(IncidentDtoIn):
+    id: UUID
+
+    start_time: datetime.datetime | None
+    end_time: datetime.datetime | None
+
+    number_of_alerts: int
+    alert_sources: list[str]
+    severity: IncidentSeverity
+    assignee: str | None
+    services: list[str]
+
+    is_predicted: bool
+
+    def __str__(self) -> str:
+        # Convert the model instance to a dictionary
+        model_dict = self.dict()
+        return json.dumps(model_dict, indent=4, default=str)
+
+    class Config:
+        extra = Extra.allow
+        schema_extra = IncidentDtoIn.Config.schema_extra
+
+        json_encoders = {
+            # Converts UUID to their values for JSON serialization
+            UUID: lambda v: str(v),
+        }
+
+    @classmethod
+    def from_db_incident(cls, db_incident):
+
+        alerts_dto = [AlertDto(**alert.event) for alert in db_incident.alerts]
+
+        unique_sources_list = list(set([
+            source
+            for alert_dto in alerts_dto
+            for source in alert_dto.source
+        ]))
+
+        return cls(
+            id=db_incident.id,
+            name=db_incident.name,
+            description=db_incident.description,
+            is_predicted=db_incident.is_predicted,
+            creation_time=db_incident.creation_time,
+            start_time=db_incident.start_time,
+            end_time=db_incident.end_time,
+            number_of_alerts=len(db_incident.alerts),
+            alert_sources=unique_sources_list,
+            severity=IncidentSeverity.CRITICAL,
+            assignee=db_incident.assignee,
+            services=["service1", "service2"],
+        )
diff --git a/keep/api/models/db/alert.py b/keep/api/models/db/alert.py
index 7f540c16c7..743028903e 100644
--- a/keep/api/models/db/alert.py
+++ b/keep/api/models/db/alert.py
@@ -82,11 +82,57 @@ def calculate_fingerprint(self):
         ).hexdigest()
 
 
+class AlertToIncident(SQLModel, table=True):
+    tenant_id: str = Field(foreign_key="tenant.id")
+    alert_id: UUID = Field(foreign_key="alert.id", primary_key=True)
+    incident_id: UUID = Field(
+        sa_column=Column(
+            UUIDType(binary=False),
+            ForeignKey("incident.id", ondelete="CASCADE"),
+            primary_key=True,
+        )
+    )
+
+
+class Incident(SQLModel, table=True):
+    id: UUID = Field(default_factory=uuid4, primary_key=True)
+    tenant_id: str = Field(foreign_key="tenant.id")
+    tenant: Tenant = Relationship()
+    name: str
+    description: str
+
+    assignee: str | None
+
+    creation_time: datetime = Field(default_factory=datetime.utcnow)
+
+    # Start/end should be calculated from first/last alerts
+    # But I suppose to have this fields as cache, to prevent extra requests
+    start_time: datetime | None
+    end_time: datetime | None
+
+    # map of attributes to values
+    alerts: List["Alert"] = Relationship(
+        back_populates="incidents", link_model=AlertToIncident
+    )
+
+    is_predicted: bool = Field(default=False)
+    is_confirmed: bool = Field(default=False)
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        if "alerts" not in kwargs:
+            self.alerts = []
+
+    class Config:
+        arbitrary_types_allowed = True
+
+
 class Alert(SQLModel, table=True):
     id: UUID = Field(default_factory=uuid4, primary_key=True)
     tenant_id: str = Field(foreign_key="tenant.id")
     tenant: Tenant = Relationship()
-    # index=True added because we query top 1000 alerts order by timestamp. On a large dataset, this will be slow without an index.
+    # index=True added because we query top 1000 alerts order by timestamp.
+    # On a large dataset, this will be slow without an index.
     #            with 1M alerts, we see queries goes from >30s to 0s with the index
     #            todo: on MSSQL, the index is "nonclustered" index which cannot be controlled by SQLModel
     timestamp: datetime = Field(
@@ -102,6 +148,9 @@ class Alert(SQLModel, table=True):
     groups: List["Group"] = Relationship(
         back_populates="alerts", link_model=AlertToGroup
     )
+    incidents: List["Incident"] = Relationship(
+        back_populates="alerts", link_model=AlertToIncident
+    )
     # alert_hash is different than fingerprint, it is a hash of the alert itself
     #            and it is used for deduplication.
     #            alert can be different but have the same fingerprint (e.g. different "firing" and "resolved" will have the same fingerprint but not the same alert_hash)
diff --git a/keep/api/models/db/migrations/script.py.mako b/keep/api/models/db/migrations/script.py.mako
index c7b8afc489..bd11d32428 100644
--- a/keep/api/models/db/migrations/script.py.mako
+++ b/keep/api/models/db/migrations/script.py.mako
@@ -8,6 +8,7 @@ Create Date: ${create_date}
 from alembic import op
 import sqlalchemy as sa
 import sqlmodel
+import sqlalchemy_utils
 
 ${imports if imports else ""}
 
diff --git a/keep/api/models/db/migrations/versions/2024-07-16-12-16_37019ca3eb2e.py b/keep/api/models/db/migrations/versions/2024-07-16-12-16_37019ca3eb2e.py
new file mode 100644
index 0000000000..43b57dd8fc
--- /dev/null
+++ b/keep/api/models/db/migrations/versions/2024-07-16-12-16_37019ca3eb2e.py
@@ -0,0 +1,66 @@
+"""Incident related tables
+
+Revision ID: 37019ca3eb2e
+Revises: c37ec8f6db3e
+Create Date: 2024-07-16 12:16:01.837477
+
+"""
+
+import sqlalchemy as sa
+import sqlmodel
+from alembic import op
+from sqlalchemy_utils import UUIDType
+
+# revision identifiers, used by Alembic.
+revision = "37019ca3eb2e"
+down_revision = "c37ec8f6db3e"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table(
+        "incident",
+        sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
+        sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("assignee", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
+        sa.Column("creation_time", sa.DateTime(), nullable=False),
+        sa.Column("start_time", sa.DateTime(), nullable=True),
+        sa.Column("end_time", sa.DateTime(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["tenant_id"],
+            ["tenant.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "alerttoincident",
+        sa.Column(
+            "incident_id",
+            UUIDType(binary=False),
+            nullable=False,
+        ),
+        sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("alert_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
+        sa.ForeignKeyConstraint(
+            ["alert_id"],
+            ["alert.id"],
+        ),
+        sa.ForeignKeyConstraint(["incident_id"], ["incident.id"], ondelete="CASCADE"),
+        sa.ForeignKeyConstraint(
+            ["tenant_id"],
+            ["tenant.id"],
+        ),
+        sa.PrimaryKeyConstraint("incident_id", "alert_id"),
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table("alerttoincident")
+    op.drop_table("incident")
+    # ### end Alembic commands ###
diff --git a/keep/api/models/db/migrations/versions/2024-07-17-16-46_dcbd2873dcfd.py b/keep/api/models/db/migrations/versions/2024-07-17-16-46_dcbd2873dcfd.py
new file mode 100644
index 0000000000..5e34e57a3c
--- /dev/null
+++ b/keep/api/models/db/migrations/versions/2024-07-17-16-46_dcbd2873dcfd.py
@@ -0,0 +1,45 @@
+"""Add is_predicted and is_confirmed flags to Incident model
+
+Revision ID: dcbd2873dcfd
+Revises: 37019ca3eb2e
+Create Date: 2024-07-17 16:46:59.386127
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.sql import expression
+
+# revision identifiers, used by Alembic.
+revision = "dcbd2873dcfd"
+down_revision = "37019ca3eb2e"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.add_column(
+        "incident",
+        sa.Column(
+            "is_confirmed",
+            sa.Boolean(),
+            nullable=False,
+            default=False,
+            server_default=expression.false(),
+        ),
+    )
+    op.add_column(
+        "incident",
+        sa.Column(
+            "is_predicted",
+            sa.Boolean(),
+            nullable=False,
+            default=False,
+            server_default=expression.false(),
+        ),
+    )
+
+
+def downgrade() -> None:
+    op.drop_column("incident", "is_confirmed")
+    op.drop_column("incident", "is_predicted")
diff --git a/keep/api/routes/ai.py b/keep/api/routes/ai.py
new file mode 100644
index 0000000000..880128cc81
--- /dev/null
+++ b/keep/api/routes/ai.py
@@ -0,0 +1,30 @@
+import os
+import logging
+
+from fastapi import (
+    APIRouter,
+    Depends,
+)
+
+from keep.api.core.dependencies import AuthenticatedEntity, AuthVerifier
+from keep.api.core.db import get_incidents_count, get_alerts_count, get_first_alert_datetime
+
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+
+
+@router.get(
+    "/stats",
+    description="Get stats for the AI Landing Page",
+)
+def get_stats(
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"]))
+):
+    tenant_id = authenticated_entity.tenant_id
+    return {
+        "alerts_count": get_alerts_count(tenant_id),
+        "first_alert_datetime": get_first_alert_datetime(tenant_id),
+        "incidents_count": get_incidents_count(tenant_id),
+        "is_mining_enabled": os.environ.get("EE_ENABLED", "false") == "true"
+    }
diff --git a/keep/api/routes/incidents.py b/keep/api/routes/incidents.py
new file mode 100644
index 0000000000..e5c84491e9
--- /dev/null
+++ b/keep/api/routes/incidents.py
@@ -0,0 +1,392 @@
+import logging
+import os
+import pathlib
+import sys
+from typing import List
+
+from fastapi import APIRouter, Depends, HTTPException, Response
+from pusher import Pusher
+from pydantic.types import UUID
+
+from keep.api.core.db import (
+    add_alerts_to_incident_by_incident_id,
+    assign_alert_to_incident,
+    create_incident_from_dto,
+    delete_incident_by_id,
+    get_incident_alerts_by_incident_id,
+    get_incident_by_id,
+    get_last_alerts,
+    get_last_incidents,
+    remove_alerts_to_incident_by_incident_id,
+    update_incident_from_dto_by_id, confirm_predicted_incident_by_id, create_incident_from_dict,
+)
+from keep.api.core.dependencies import (
+    AuthenticatedEntity,
+    AuthVerifier,
+    get_pusher_client,
+)
+from keep.api.models.alert import AlertDto, IncidentDto, IncidentDtoIn
+from keep.api.utils.enrichment_helpers import convert_db_alerts_to_dto_alerts
+from keep.api.utils.pagination import IncidentsPaginatedResultsDto
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+
+ee_enabled = os.environ.get("EE_ENABLED", "false") == "true"
+if ee_enabled or 1:
+    path_with_ee = (
+        str(pathlib.Path(__file__).parent.resolve()) + "/../../../ee/experimental"
+    )
+    sys.path.insert(0, path_with_ee)
+    from incident_utils import mine_incidents  # noqa
+
+
+def __update_client_on_incident_change(
+    pusher_client: Pusher | None, tenant_id: str, incident_id: str | None = None
+):
+    """
+    Update the client on incident change, making the client poll changes.
+
+    Args:
+        pusher_client (Pusher | None): Pusher client if pusher is enabled.
+        tenant_id (str): Tenant id.
+        incident_id (str | None, optional): If this is relevant to a specific incident id. Defaults to None.
+            E.g., when someone correlates new alerts to an incident, we want to notify the client that the incident has changed.
+    """
+    if pusher_client is not None:
+        logger.info(
+            "Notifying client on incident change",
+            extra={"tenant_id": tenant_id, "incident_id": incident_id},
+        )
+        pusher_client.trigger(
+            f"private-{tenant_id}",
+            "incident-change",
+            {"incident_id": incident_id},
+        )
+        logger.info(
+            "Client notified on incident change",
+            extra={"tenant_id": tenant_id, "incident_id": incident_id},
+        )
+    else:
+        logger.debug(
+            "Pusher client not available, skipping incident change notification"
+        )
+
+
+@router.post(
+    "",
+    description="Create new incident",
+    status_code=202,
+    response_model=IncidentDto,
+)
+def create_incident_endpoint(
+    incident_dto: IncidentDtoIn,
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+    pusher_client: Pusher | None = Depends(get_pusher_client),
+) -> IncidentDto:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Creating incidents in DB",
+        extra={
+            "tenant_id": tenant_id,
+        },
+    )
+    incident = create_incident_from_dto(tenant_id, incident_dto)
+    new_incident_dto = IncidentDto.from_db_incident(incident)
+    logger.info(
+        "New incident created in DB",
+        extra={
+            "tenant_id": tenant_id,
+        },
+    )
+    __update_client_on_incident_change(pusher_client, tenant_id)
+    return new_incident_dto
+
+
+@router.get(
+    "",
+    description="Get last incidents",
+)
+def get_all_incidents(
+    confirmed: bool = True,
+    limit: int = 25,
+    offset: int = 0,
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+) -> IncidentsPaginatedResultsDto:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incidents from DB",
+        extra={
+            "tenant_id": tenant_id,
+        },
+    )
+    incidents, total_count = get_last_incidents(
+        tenant_id=tenant_id,
+        is_confirmed=confirmed,
+        limit=limit,
+        offset=offset,
+    )
+
+    incidents_dto = []
+    for incident in incidents:
+        incidents_dto.append(IncidentDto.from_db_incident(incident))
+
+    logger.info(
+        "Fetched incidents from DB",
+        extra={
+            "tenant_id": tenant_id,
+        },
+    )
+
+    return IncidentsPaginatedResultsDto(
+        limit=limit,
+        offset=offset,
+        count=total_count,
+        items=incidents_dto
+    )
+
+
+@router.get(
+    "/{incident_id}",
+    description="Get incident by id",
+)
+def get_incident(
+    incident_id: str,
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+) -> IncidentDto:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incident",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+    incident = get_incident_by_id(tenant_id=tenant_id, incident_id=incident_id)
+    if not incident:
+        raise HTTPException(status_code=404, detail="Incident not found")
+
+    incident_dto = IncidentDto.from_db_incident(incident)
+
+    return incident_dto
+
+
+@router.put(
+    "/{incident_id}",
+    description="Update incident by id",
+)
+def update_incident(
+    incident_id: str,
+    updated_incident_dto: IncidentDtoIn,
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+) -> IncidentDto:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incident",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+
+    incident = update_incident_from_dto_by_id(
+        tenant_id, incident_id, updated_incident_dto
+    )
+    if not incident:
+        raise HTTPException(status_code=404, detail="Incident not found")
+
+    new_incident_dto = IncidentDto.from_db_incident(incident)
+
+    return new_incident_dto
+
+
+@router.delete(
+    "/{incident_id}",
+    description="Delete incident by incident id",
+)
+def delete_incident(
+    incident_id: str,
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+    pusher_client: Pusher | None = Depends(get_pusher_client),
+):
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incident",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+    deleted = delete_incident_by_id(tenant_id=tenant_id, incident_id=incident_id)
+    if not deleted:
+        raise HTTPException(status_code=404, detail="Incident not found")
+    __update_client_on_incident_change(pusher_client, tenant_id)
+    return Response(status_code=202)
+
+
+@router.get(
+    "/{incident_id}/alerts",
+    description="Get incident alerts by incident incident id",
+)
+def get_incident_alerts(
+    incident_id: str,
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+) -> List[AlertDto]:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incident",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+    incident = get_incident_by_id(tenant_id=tenant_id, incident_id=incident_id)
+    if not incident:
+        raise HTTPException(status_code=404, detail="Incident not found")
+
+    logger.info(
+        "Fetching incident's alert",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+    db_alerts = get_incident_alerts_by_incident_id(tenant_id, incident_id)
+
+    enriched_alerts_dto = convert_db_alerts_to_dto_alerts(db_alerts)
+    logger.info(
+        "Fetched alerts from DB",
+        extra={
+            "tenant_id": tenant_id,
+        },
+    )
+
+    return enriched_alerts_dto
+
+
+@router.post(
+    "/{incident_id}/alerts",
+    description="Add alerts to incident",
+    status_code=202,
+    response_model=List[AlertDto],
+)
+def add_alerts_to_incident(
+    incident_id: str,
+    alert_ids: List[UUID],
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+    pusher_client: Pusher | None = Depends(get_pusher_client),
+):
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incident",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+    incident = get_incident_by_id(tenant_id=tenant_id, incident_id=incident_id)
+    if not incident:
+        raise HTTPException(status_code=404, detail="Incident not found")
+
+    add_alerts_to_incident_by_incident_id(tenant_id, incident_id, alert_ids)
+    __update_client_on_incident_change(pusher_client, tenant_id, incident_id)
+
+    return Response(status_code=202)
+
+
+@router.delete(
+    "/{incident_id}/alerts",
+    description="Delete alerts from incident",
+    status_code=202,
+    response_model=List[AlertDto],
+)
+def delete_alerts_from_incident(
+    incident_id: str,
+    alert_ids: List[UUID],
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+):
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incident",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+    incident = get_incident_by_id(tenant_id=tenant_id, incident_id=incident_id)
+    if not incident:
+        raise HTTPException(status_code=404, detail="Incident not found")
+
+    remove_alerts_to_incident_by_incident_id(tenant_id, incident_id, alert_ids)
+
+    return Response(status_code=202)
+
+
+@router.post(
+    "/mine",
+    description="Create incidents using historical alerts",
+)
+def mine(
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier()),
+    use_n_historical_alerts: int = 10000,
+    incident_sliding_window_size: int = 6 * 24 * 60 * 60,
+    statistic_sliding_window_size: int = 60 * 60,
+    jaccard_threshold: float = 0.0,
+    fingerprint_threshold: int = 1,
+) -> dict:
+    tenant_id = authenticated_entity.tenant_id
+    alerts = get_last_alerts(tenant_id, limit=use_n_historical_alerts)
+
+    if len(alerts) == 0:
+        return {"incidents": []}
+
+    incidents = mine_incidents(
+        alerts,
+        incident_sliding_window_size,
+        statistic_sliding_window_size,
+        jaccard_threshold,
+        fingerprint_threshold,
+    )
+    if len(incidents) == 0:
+        return {"incidents": []}
+
+    for incident in incidents:
+        incident_id = create_incident_from_dict(
+            tenant_id=tenant_id,
+            incident_data={
+                "name": "Mined using algorithm",
+                "description": "Candidate",
+                "is_predicted": True
+            }
+        ).id
+
+        for alert in incident["alerts"]:
+            assign_alert_to_incident(alert.id, incident_id, tenant_id)
+
+    return {"incidents": incidents}
+
+
+@router.post(
+    "/{incident_id}/confirm",
+    description="Confirm predicted incident by id",
+)
+def update_incident(
+    incident_id: str,
+    authenticated_entity: AuthenticatedEntity = Depends(AuthVerifier(["read:alert"])),
+) -> IncidentDto:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info(
+        "Fetching incident",
+        extra={
+            "incident_id": incident_id,
+            "tenant_id": tenant_id,
+        },
+    )
+
+    incident = confirm_predicted_incident_by_id(tenant_id, incident_id)
+    if not incident:
+        raise HTTPException(status_code=404, detail="Incident candidate not found")
+
+    new_incident_dto = IncidentDto.from_db_incident(incident)
+
+    return new_incident_dto
diff --git a/keep/api/utils/enrichment_helpers.py b/keep/api/utils/enrichment_helpers.py
index 2b1a3288b0..beb5e7e10f 100644
--- a/keep/api/utils/enrichment_helpers.py
+++ b/keep/api/utils/enrichment_helpers.py
@@ -71,6 +71,11 @@ def convert_db_alerts_to_dto_alerts(alerts: list[Alert]) -> list[AlertDto]:
                     },
                 )
                 continue
+
+            # include the db event id if it's not present
+            if alert_dto.event_id is None:
+                alert_dto.event_id = str(alert.id)
+
             # enrich provider id when it's possible
             if alert_dto.providerId is None:
                 alert_dto.providerId = alert.provider_id
diff --git a/keep/api/utils/pagination.py b/keep/api/utils/pagination.py
new file mode 100644
index 0000000000..cac63e9057
--- /dev/null
+++ b/keep/api/utils/pagination.py
@@ -0,0 +1,16 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from keep.api.models.alert import IncidentDto
+
+
+class PaginatedResultsDto(BaseModel):
+    limit: int = 25
+    offset: int = 0
+    count: int
+    items: list[Any]
+
+
+class IncidentsPaginatedResultsDto(PaginatedResultsDto):
+    items: list[IncidentDto]
diff --git a/poetry.lock b/poetry.lock
index f302869bbe..6f1282972e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2416,6 +2416,24 @@ files = [
     {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
 ]
 
+[[package]]
+name = "networkx"
+version = "3.3"
+description = "Python package for creating and manipulating graphs and networks"
+optional = false
+python-versions = ">=3.10"
+files = [
+    {file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"},
+    {file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"},
+]
+
+[package.extras]
+default = ["matplotlib (>=3.6)", "numpy (>=1.23)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"]
+developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"]
+doc = ["myst-nb (>=1.0)", "numpydoc (>=1.7)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"]
+extra = ["lxml (>=4.6)", "pydot (>=2.0)", "pygraphviz (>=1.12)", "sympy (>=1.10)"]
+test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
+
 [[package]]
 name = "nodeenv"
 version = "1.9.1"
@@ -2427,6 +2445,60 @@ files = [
     {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
 ]
 
+[[package]]
+name = "numpy"
+version = "2.0.0"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"},
+    {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"},
+    {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"},
+    {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"},
+    {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"},
+    {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"},
+    {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"},
+    {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"},
+    {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"},
+    {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"},
+    {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"},
+    {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"},
+    {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"},
+    {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"},
+    {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"},
+    {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"},
+    {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"},
+    {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"},
+    {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"},
+    {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"},
+    {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"},
+    {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"},
+    {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"},
+    {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"},
+    {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"},
+    {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"},
+    {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"},
+    {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"},
+    {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"},
+    {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"},
+    {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"},
+    {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"},
+    {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"},
+    {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"},
+    {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"},
+    {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"},
+    {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"},
+    {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"},
+    {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"},
+    {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"},
+    {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"},
+    {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"},
+    {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"},
+    {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"},
+    {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"},
+]
+
 [[package]]
 name = "oauthlib"
 version = "3.2.2"
@@ -2826,6 +2898,75 @@ files = [
     {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
 ]
 
+[[package]]
+name = "pandas"
+version = "2.2.2"
+description = "Powerful data structures for data analysis, time series, and statistics"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"},
+    {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"},
+    {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"},
+    {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"},
+    {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"},
+    {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"},
+    {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"},
+    {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"},
+    {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"},
+    {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"},
+    {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"},
+    {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"},
+    {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"},
+    {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"},
+    {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"},
+    {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"},
+    {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"},
+    {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"},
+    {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"},
+    {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"},
+    {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"},
+    {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"},
+    {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"},
+    {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"},
+    {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"},
+    {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"},
+    {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"},
+    {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"},
+    {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"},
+]
+
+[package.dependencies]
+numpy = {version = ">=1.23.2", markers = "python_version == \"3.11\""}
+python-dateutil = ">=2.8.2"
+pytz = ">=2020.1"
+tzdata = ">=2022.7"
+
+[package.extras]
+all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"]
+aws = ["s3fs (>=2022.11.0)"]
+clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"]
+compression = ["zstandard (>=0.19.0)"]
+computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"]
+consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
+excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"]
+feather = ["pyarrow (>=10.0.1)"]
+fss = ["fsspec (>=2022.11.0)"]
+gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"]
+hdf5 = ["tables (>=3.8.0)"]
+html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"]
+mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"]
+output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"]
+parquet = ["pyarrow (>=10.0.1)"]
+performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"]
+plot = ["matplotlib (>=3.6.3)"]
+postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"]
+pyarrow = ["pyarrow (>=10.0.1)"]
+spss = ["pyreadstat (>=1.2.0)"]
+sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"]
+test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
+xml = ["lxml (>=4.9.2)"]
+
 [[package]]
 name = "paramiko"
 version = "3.4.0"
@@ -4536,6 +4677,17 @@ files = [
     {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 ]
 
+[[package]]
+name = "tzdata"
+version = "2024.1"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+    {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+    {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+]
+
 [[package]]
 name = "uptime-kuma-api"
 version = "1.2.1"
@@ -4893,4 +5045,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.11,<3.12"
-content-hash = "9e16f7d9545258b69d39fe1578338a74514d609e2fc31818b5c7f41109e4e7c9"
+content-hash = "b02155f81098680deab94ff5d9d126f696e11837f0a95442ed78b3d25bbc0554"
diff --git a/pyproject.toml b/pyproject.toml
index b9f654b2d0..960f993359 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -86,6 +86,9 @@ arq = "^0.26.0"
 
 
 alembic = "^1.13.2"
+numpy = "^2.0.0"
+pandas = "^2.2.2"
+networkx = "^3.3"
 [tool.poetry.group.dev.dependencies]
 pre-commit = "^3.0.4"
 pre-commit-hooks = "^4.4.0"
diff --git a/scripts/shoot_alerts_from_dump.py b/scripts/shoot_alerts_from_dump.py
new file mode 100644
index 0000000000..4e3db0476c
--- /dev/null
+++ b/scripts/shoot_alerts_from_dump.py
@@ -0,0 +1,77 @@
+import logging
+import os, sys
+import json
+import copy
+import csv
+
+
+from keep.api.core.db import get_session_sync
+from keep.api.models.alert import AlertDto
+from keep.api.tasks.process_event_task import __handle_formatted_events
+import sys
+import argparse
+
+# configure logging
+logging.basicConfig(
+    level=logging.DEBUG,
+    format="%(asctime)s %(levelname)s %(name)s %(message)s",
+    datefmt="%Y-%m-%d %H:%M:%S",
+)
+
+logger = logging.getLogger(__name__)
+
+def count_alerts_for_tenants(csv_file):
+        tenants = {}
+        with open(csv_file, 'r') as file:
+            reader = csv.reader(file)
+            progress = 0
+            for row in reader:
+                progress += 1
+                if progress % 10000 == 0:
+                    print(f"Processing row {progress}")
+                if len(row) > 2:
+                    tenants[row[2]] = tenants.get(row[2], 0) + 1
+        print("Tenants and their alerts count:")
+        print({k: v for k, v in sorted(tenants.items(), key=lambda item: item[1], reverse=True)})
+
+
+def shoot_tenants_alerts(file, tenant_id):
+    new_tenant_id = "keep"
+    session = get_session_sync()
+    with open(file, 'r') as file:
+        reader = csv.reader(file)
+        file.seek(0)
+
+        for row in reader:
+            if len(row) > 2 and row[2] == tenant_id:
+                alert = json.loads(row[0])
+                alert["tenant_id"] = new_tenant_id
+                raw_event = [copy.deepcopy(alert)]
+                alert = AlertDto(**alert)
+                __handle_formatted_events(
+                    new_tenant_id,
+                    provider_type=row[2],
+                    session=session,
+                    raw_events=raw_event,
+                    formatted_events=[alert],
+                )
+    session.close()
+
+
+if __name__ == "__main__":
+    
+
+    parser = argparse.ArgumentParser(description="Shoot alerts from dump")
+    parser.add_argument("csv_file", help="Path to the CSV file")
+    parser.add_argument("--tenant-id", help="ID of the tenant, if no tenant_id is provided, listing tenants.")
+
+    args = parser.parse_args()
+
+    csv_file = args.csv_file
+    tenant_id = args.tenant_id
+    csv.field_size_limit(sys.maxsize)
+    
+    if tenant_id is None:
+        count_alerts_for_tenants(csv_file)
+    else:
+        shoot_tenants_alerts(csv_file, tenant_id)


Code review comments:
diff_hunk: @@ -0,0 +1,392 @@
+import logging
+import os
+import pathlib
+import sys
+from typing import List
+
+from fastapi import APIRouter, Depends, HTTPException, Response
+from pusher import Pusher
+from pydantic.types import UUID
+
+from keep.api.core.db import (
+    add_alerts_to_incident_by_incident_id,
+    assign_alert_to_incident,
+    create_incident_from_dto,
+    delete_incident_by_id,
+    get_incident_alerts_by_incident_id,
+    get_incident_by_id,
+    get_last_alerts,
+    get_last_incidents,
+    remove_alerts_to_incident_by_incident_id,
+    update_incident_from_dto_by_id, confirm_predicted_incident_by_id, create_incident_from_dict,
+)
+from keep.api.core.dependencies import (
+    AuthenticatedEntity,
+    AuthVerifier,
+    get_pusher_client,
+)
+from keep.api.models.alert import AlertDto, IncidentDto, IncidentDtoIn
+from keep.api.utils.enrichment_helpers import convert_db_alerts_to_dto_alerts
+from keep.api.utils.pagination import IncidentsPaginatedResultsDto
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+
+ee_enabled = os.environ.get("EE_ENABLED", "false") == "true"
+if ee_enabled or 1:
+    path_with_ee = (
+        str(pathlib.Path(__file__).parent.resolve()) + "/../../../ee/experimental"
path: keep/api/routes/incidents.py
text: @user1:
99% will break soon

@user2:
I'm not sure how to guarantee import without such a relative import. If it will be an absolute import, it will depend on the folder we launch the app from. I could add a unit test

diff_hunk: @@ -0,0 +1,152 @@
+"use client";
+import { Card, List, ListItem, Title, Subtitle } from "@tremor/react";
+import { useAIStats } from "utils/hooks/useAIStats";
+import { useSession } from "next-auth/react";
+import { getApiURL } from "utils/apiUrl";
+import { toast } from "react-toastify";
+import { useEffect, useState, useRef, FormEvent } from "react";
+
+export default function Ai() {
+  const { data: aistats, isLoading } = useAIStats();
+  const { data: session } = useSession();
+  const [text, setText] = useState("");
+  const [newText, setNewText] = useState("Mine incidents");
+  const [animate, setAnimate] = useState(false);
+  const onlyOnce = useRef(false);
+
+  useEffect(() => {
+    let index = 0;
+
+    const interval = setInterval(() => {
+      setText(newText.slice(0, index + 1));
+      index++;
+
+      if (index === newText.length) {
+        clearInterval(interval);
+      }
+    }, 100);
+
+    return () => {
+      clearInterval(interval);
+    };
+  }, [newText]);
+
+  const mineIncidents = async (e: FormEvent) => {
+    e.preventDefault();
+    setAnimate(true);
+    setNewText("Mining 🚀🚀🚀 ...");
+    const apiUrl = getApiURL();
+    const response = await fetch(`${apiUrl}/incidents/mine`, {
+      method: "POST",
+      headers: {
+        Authorization: `Bearer ${session?.accessToken}`,
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+      }),
+    });
+    if (response.ok) {
+      setAnimate(false);
+      setNewText("Mine incidents");
+    } else {
+      setAnimate(false);
+      setNewText("Mine incidents");
path: keep-ui/app/ai/ai.tsx
text: @user1:
?

both response.ok or !response.okl are basically doing the same?