From a2bcbe9430906078646ce3bc9c4b186c86652a04 Mon Sep 17 00:00:00 2001
From: root <root@Goutam.localdomain>
Date: Wed, 25 Mar 2026 06:03:09 +0000
Subject: [PATCH 1/7] Add time range selector to Gantt view

---
 .../ui/src/layouts/Details/Gantt/Gantt.tsx    | 105 ++++++++++++++----
 1 file changed, 84 insertions(+), 21 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
index 62de32cdb98bb..1de108c04ba68 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, useToken } from "@chakra-ui/react";
+import { Box, Input, Text, useToken } from "@chakra-ui/react";
 import {
   Chart as ChartJS,
   CategoryScale,
@@ -33,11 +33,10 @@ import {
 import "chart.js/auto";
 import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm";
 import annotationPlugin from "chartjs-plugin-annotation";
-import { useDeferredValue } from "react";
+import { useDeferredValue, useState } from "react";
 import { Bar } from "react-chartjs-2";
 import { useTranslation } from "react-i18next";
 import { useParams, useNavigate, useLocation, useSearchParams } from "react-router-dom";
-
 import { useGanttServiceGetGanttData } from "openapi/queries";
 import type { DagRunState, DagRunType } from "openapi/requests/types.gen";
 import { useColorMode } from "src/context/colorMode";
@@ -83,6 +82,8 @@ const MIN_BAR_WIDTH = 10;
 
 export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, triggeringUser }: Props) => {
   const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams();
+  const [filterStartDate, setFilterStartDate] = useState("");
+  const [filterEndDate, setFilterEndDate] = useState("");
   const [searchParams] = useSearchParams();
   const { openGroupIds } = useOpenGroups();
   const deferredOpenGroupIds = useDeferredValue(openGroupIds);
@@ -161,9 +162,36 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   const allTries = ganttData?.task_instances ?? [];
   const gridSummaries = gridTiSummaries?.task_instances ?? [];
 
-  const data = isLoading || runId === "" ? [] : transformGanttData({ allTries, flatNodes, gridSummaries });
+  const rawData =
+    isLoading || runId === ""
+      ? []
+      : transformGanttData({ allTries, flatNodes, gridSummaries });
+
+  // Filter data by date range if specified
+  const data = rawData.filter((task) => {
+    if (!filterStartDate && !filterEndDate) return true;
+
+    // task.x contains [start_date, end_date] as ISO strings
+    const taskStartDate = task.x[0];
+    const taskEndDate = task.x[1];
+
+    if (!taskStartDate) return true;
 
-  const labels = flatNodes.map((node) => node.id);
+    if (filterStartDate && taskEndDate && new Date(taskEndDate) < new Date(filterStartDate)) return false;
+    if (filterEndDate && new Date(taskStartDate) > new Date(filterEndDate + "T23:59:59")) return false;
+
+    return true;
+  });
+
+  // Get unique task IDs from filtered data for labels
+  const filteredTaskIds = new Set(data.map((item) => item.taskId));
+  const labels = flatNodes
+    .filter((node) => {
+      // Show all tasks if no filter, otherwise only show filtered tasks
+      if (!filterStartDate && !filterEndDate) return true;
+      return filteredTaskIds.has(node.id);
+    })
+    .map((node) => node.id);
 
   // Get all unique states and their colors
   const states = [...new Set(data.map((item) => item.state ?? "none"))];
@@ -228,21 +256,56 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   };
 
   return (
-    <Box
-      height={`${fixedHeight}px`}
-      minW="250px"
-      ml={-2}
-      mt={`${GRID_BODY_OFFSET_PX}px`}
-      onMouseLeave={handleChartMouseLeave}
-      w="100%"
-    >
-      <Bar
-        data={chartData}
-        options={chartOptions}
-        style={{
-          paddingTop: flatNodes.length === 1 ? 15 : 1.5,
-        }}
-      />
-    </Box>
+    <>
+      <Box mb="4" display="flex" gap="4" alignItems="flex-end">
+        <Box display="flex" flexDirection="column">
+          <Text color="fg.muted" fontSize="xs" mb={1}>
+            {translate("startDate")}
+          </Text>
+          <Input
+            fontSize="sm"
+            fontWeight="medium"
+            maxW="200px"
+            onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterStartDate(e.target.value)}
+            placeholder="YYYY-MM-DD"
+            size="sm"
+            type="date"
+            value={filterStartDate}
+          />
+        </Box>
+        <Box display="flex" flexDirection="column">
+          <Text color="fg.muted" fontSize="xs" mb={1}>
+            {translate("endDate")}
+          </Text>
+          <Input
+            fontSize="sm"
+            fontWeight="medium"
+            maxW="200px"
+            onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterEndDate(e.target.value)}
+            placeholder="YYYY-MM-DD"
+            size="sm"
+            type="date"
+            value={filterEndDate}
+          />
+        </Box>
+      </Box>
+
+      <Box
+        height={`${fixedHeight}px`}
+        minW="250px"
+        ml={-2}
+        mt={`${GRID_BODY_OFFSET_PX}px`}
+        onMouseLeave={handleChartMouseLeave}
+        w="100%"
+      >
+        <Bar
+          data={chartData}
+          options={chartOptions}
+          style={{
+            paddingTop: flatNodes.length === 1 ? 15 : 1.5,
+          }}
+        />
+      </Box>
+    </>
   );
 };

From a71aa5b610d6f1f08aaffa804121859a4b5cbf6c Mon Sep 17 00:00:00 2001
From: root <root@Goutam.localdomain>
Date: Sat, 28 Mar 2026 21:08:32 +0000
Subject: [PATCH 2/7] Add date range filter to Gantt view with server-side
 filtering and validation

---
 .../api_fastapi/core_api/routes/ui/gantt.py   | 36 ++++++++++-
 .../ui/src/layouts/Details/Gantt/Gantt.tsx    | 64 +++++++++----------
 2 files changed, 64 insertions(+), 36 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
index f33b12e6f7e8a..231f09615072b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
@@ -17,7 +17,10 @@
 
 from __future__ import annotations
 
-from fastapi import Depends, HTTPException, status
+from datetime import datetime
+from typing import Annotated
+
+from fastapi import Depends, HTTPException, Query, status
 from sqlalchemy import or_, select, union_all
 
 from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity
@@ -59,8 +62,17 @@ def get_gantt_data(
     dag_id: str,
     run_id: str,
     session: SessionDep,
+    start_date: Annotated[datetime | None, Query()] = None,
+    end_date: Annotated[datetime | None, Query()] = None,
 ) -> GanttResponse:
+   
     """Get all task instance tries for Gantt chart."""
+    
+    if start_date and end_date and start_date > end_date:
+        raise HTTPException(
+            status.HTTP_400_BAD_REQUEST,
+            "start_date cannot be greater than end_date",
+        )
     # Exclude mapped tasks (use grid summaries) and UP_FOR_RETRY (already in history)
     current_tis = select(
         TaskInstance.task_id.label("task_id"),
@@ -74,6 +86,16 @@ def get_gantt_data(
         TaskInstance.run_id == run_id,
         TaskInstance.map_index == -1,
         or_(TaskInstance.state != TaskInstanceState.UP_FOR_RETRY, TaskInstance.state.is_(None)),
+        *(
+            [TaskInstance.start_date <= end_date]
+            if end_date is not None
+            else []
+        ),
+        *(
+            [TaskInstance.end_date >= start_date]
+            if start_date is not None
+            else []
+        ),
     )
 
     history_tis = select(
@@ -87,6 +109,16 @@ def get_gantt_data(
         TaskInstanceHistory.dag_id == dag_id,
         TaskInstanceHistory.run_id == run_id,
         TaskInstanceHistory.map_index == -1,
+        *(
+            [TaskInstanceHistory.start_date <= end_date]
+            if end_date is not None
+            else []
+        ),
+        *(
+            [TaskInstanceHistory.end_date >= start_date]
+            if start_date is not None
+            else []
+        ),
     )
 
     combined = union_all(current_tis, history_tis).subquery()
@@ -112,4 +144,4 @@ def get_gantt_data(
         for row in results
     ]
 
-    return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances)
+    return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances)
\ No newline at end of file
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
index 1de108c04ba68..b597019b8c983 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
@@ -37,6 +37,7 @@ import { useDeferredValue, useState } from "react";
 import { Bar } from "react-chartjs-2";
 import { useTranslation } from "react-i18next";
 import { useParams, useNavigate, useLocation, useSearchParams } from "react-router-dom";
+
 import { useGanttServiceGetGanttData } from "openapi/queries";
 import type { DagRunState, DagRunType } from "openapi/requests/types.gen";
 import { useColorMode } from "src/context/colorMode";
@@ -84,6 +85,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams();
   const [filterStartDate, setFilterStartDate] = useState("");
   const [filterEndDate, setFilterEndDate] = useState("");
+
   const [searchParams] = useSearchParams();
   const { openGroupIds } = useOpenGroups();
   const deferredOpenGroupIds = useDeferredValue(openGroupIds);
@@ -145,8 +147,15 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   const summariesLoading = Boolean(runId && selectedRun && !summariesByRunId.has(runId));
 
   // Single fetch for all Gantt data (individual task tries)
+  // startDate and endDate are sent to the backend as query parameters.
+  // The server filters the data — NOT the browser.
   const { data: ganttData, isLoading: ganttLoading } = useGanttServiceGetGanttData(
-    { dagId, runId },
+    {
+      dagId,
+      runId,
+      startDate: filterStartDate ? `${filterStartDate}T00:00:00Z` : undefined,
+      endDate: filterEndDate ? `${filterEndDate}T23:59:59Z` : undefined,
+    },
     undefined,
     {
       enabled: Boolean(dagId) && Boolean(runId) && Boolean(selectedRun),
@@ -162,36 +171,9 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   const allTries = ganttData?.task_instances ?? [];
   const gridSummaries = gridTiSummaries?.task_instances ?? [];
 
-  const rawData =
-    isLoading || runId === ""
-      ? []
-      : transformGanttData({ allTries, flatNodes, gridSummaries });
-
-  // Filter data by date range if specified
-  const data = rawData.filter((task) => {
-    if (!filterStartDate && !filterEndDate) return true;
-
-    // task.x contains [start_date, end_date] as ISO strings
-    const taskStartDate = task.x[0];
-    const taskEndDate = task.x[1];
+  const data = isLoading || runId === "" ? [] : transformGanttData({ allTries, flatNodes, gridSummaries });
 
-    if (!taskStartDate) return true;
-
-    if (filterStartDate && taskEndDate && new Date(taskEndDate) < new Date(filterStartDate)) return false;
-    if (filterEndDate && new Date(taskStartDate) > new Date(filterEndDate + "T23:59:59")) return false;
-
-    return true;
-  });
-
-  // Get unique task IDs from filtered data for labels
-  const filteredTaskIds = new Set(data.map((item) => item.taskId));
-  const labels = flatNodes
-    .filter((node) => {
-      // Show all tasks if no filter, otherwise only show filtered tasks
-      if (!filterStartDate && !filterEndDate) return true;
-      return filteredTaskIds.has(node.id);
-    })
-    .map((node) => node.id);
+  const labels = flatNodes.map((node) => node.id);
 
   // Get all unique states and their colors
   const states = [...new Set(data.map((item) => item.state ?? "none"))];
@@ -257,6 +239,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
 
   return (
     <>
+      {/* Date inputs that trigger a new API fetch when changed */}
       <Box mb="4" display="flex" gap="4" alignItems="flex-end">
         <Box display="flex" flexDirection="column">
           <Text color="fg.muted" fontSize="xs" mb={1}>
@@ -266,7 +249,14 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
             fontSize="sm"
             fontWeight="medium"
             maxW="200px"
-            onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterStartDate(e.target.value)}
+            onChange={(e) => {
+              const value = e.target.value;
+              if (filterEndDate && value > filterEndDate) {
+                alert("Start date cannot be after end date");
+                return;
+              }
+              setFilterStartDate(value);
+            }}
             placeholder="YYYY-MM-DD"
             size="sm"
             type="date"
@@ -281,7 +271,14 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
             fontSize="sm"
             fontWeight="medium"
             maxW="200px"
-            onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterEndDate(e.target.value)}
+            onChange={(e) => {
+              const value = e.target.value;
+              if (filterStartDate && value < filterStartDate) {
+                alert("End date cannot be before start date");
+                return;
+              }
+              setFilterEndDate(value);
+            }}
             placeholder="YYYY-MM-DD"
             size="sm"
             type="date"
@@ -289,7 +286,6 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
           />
         </Box>
       </Box>
-
       <Box
         height={`${fixedHeight}px`}
         minW="250px"
@@ -308,4 +304,4 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
       </Box>
     </>
   );
-};
+};
\ No newline at end of file

From 28846e9c94b35d5a9f690541bafb3b421fc15486 Mon Sep 17 00:00:00 2001
From: root <root@Goutam.localdomain>
Date: Sun, 29 Mar 2026 10:39:16 +0000
Subject: [PATCH 3/7] Add newsfragment for Gantt date filter

---
 newsfragments/64387.significant.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 newsfragments/64387.significant.rst

diff --git a/newsfragments/64387.significant.rst b/newsfragments/64387.significant.rst
new file mode 100644
index 0000000000000..a2a2b2d4acf8c
--- /dev/null
+++ b/newsfragments/64387.significant.rst
@@ -0,0 +1 @@
+Add start date and end date filter to the Gantt view with server-side filtering.

From e46373c6e25bd643813efab864329f66c55e1dac Mon Sep 17 00:00:00 2001
From: root <root@Goutam.localdomain>
Date: Sun, 29 Mar 2026 10:56:31 +0000
Subject: [PATCH 4/7] Final fixes

---
 .../api_fastapi/core_api/routes/ui/gantt.py   | 14 ++---------
 .../core_api/routes/ui/test_gantt.py          | 24 +++++++++++++++++++
 2 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
index 231f09615072b..cbadd8bb3e4f3 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
@@ -87,12 +87,7 @@ def get_gantt_data(
         TaskInstance.map_index == -1,
         or_(TaskInstance.state != TaskInstanceState.UP_FOR_RETRY, TaskInstance.state.is_(None)),
         *(
-            [TaskInstance.start_date <= end_date]
-            if end_date is not None
-            else []
-        ),
-        *(
-            [TaskInstance.end_date >= start_date]
+            [or_(TaskInstance.end_date >= start_date, TaskInstance.end_date.is_(None))]
             if start_date is not None
             else []
         ),
@@ -110,12 +105,7 @@ def get_gantt_data(
         TaskInstanceHistory.run_id == run_id,
         TaskInstanceHistory.map_index == -1,
         *(
-            [TaskInstanceHistory.start_date <= end_date]
-            if end_date is not None
-            else []
-        ),
-        *(
-            [TaskInstanceHistory.end_date >= start_date]
+            [or_(TaskInstanceHistory.end_date >= start_date, TaskInstanceHistory.end_date.is_(None))]
             if start_date is not None
             else []
         ),
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
index 162c82682afcf..0e942efacf127 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
@@ -326,3 +326,27 @@ def test_should_response_404(self, test_client, dag_id, run_id):
         with assert_queries_count(3):
             response = test_client.get(f"/gantt/{dag_id}/{run_id}")
         assert response.status_code == 404
+
+    def test_gantt_with_start_date(self, test_client):
+        response = test_client.get(
+            f"/gantt/{DAG_ID}/run_1",
+            params={"start_date": "2024-11-30T10:06:00"},
+        )
+        assert response.status_code == 200
+
+        data = response.json()
+
+        # running task (NULL end_date) should STILL be included
+        task_ids = [ti["task_id"] for ti in data["task_instances"]]
+        assert "task3" in task_ids
+
+
+    def test_gantt_invalid_date_range(self, test_client):
+        response = test_client.get(
+            f"/gantt/{DAG_ID}/run_1",
+            params={
+                "start_date": "2024-12-01T00:00:00",
+                "end_date": "2024-11-01T00:00:00",
+            },
+        )
+        assert response.status_code == 400
\ No newline at end of file

From 58250f436bb49d3460c056f7a05913b4711d527b Mon Sep 17 00:00:00 2001
From: root <root@Goutam.localdomain>
Date: Sun, 29 Mar 2026 11:07:15 +0000
Subject: [PATCH 5/7] Final fixes

---
 .../airflow/api_fastapi/core_api/routes/ui/gantt.py    | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
index cbadd8bb3e4f3..2af01b44b3f6b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
@@ -91,6 +91,11 @@ def get_gantt_data(
             if start_date is not None
             else []
         ),
+        *(
+            [TaskInstance.start_date <= end_date]
+            if end_date is not None
+            else []
+        ),
     )
 
     history_tis = select(
@@ -109,6 +114,11 @@ def get_gantt_data(
             if start_date is not None
             else []
         ),
+        *(
+            [TaskInstanceHistory.start_date <= end_date]
+            if end_date is not None
+            else []
+        ),
     )
 
     combined = union_all(current_tis, history_tis).subquery()

From c57cbca3e7fc7e607b680e647360b4ccc61e6998 Mon Sep 17 00:00:00 2001
From: root <root@Goutam.localdomain>
Date: Mon, 6 Apr 2026 18:49:03 +0000
Subject: [PATCH 6/7] fix: address review comments - use Field.Root for Chakra
 v3, fix query count, improve tests

---
 .../api_fastapi/core_api/routes/ui/gantt.py   | 27 +++++-----
 .../ui/public/i18n/locales/en/common.json     |  2 +
 .../ui/src/layouts/Details/Gantt/Gantt.tsx    | 40 ++++++++-------
 .../core_api/routes/ui/test_gantt.py          | 50 ++++++++++++++++---
 newsfragments/64387.significant.rst           |  1 -
 5 files changed, 81 insertions(+), 39 deletions(-)
 delete mode 100644 newsfragments/64387.significant.rst

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
index 2af01b44b3f6b..088e99c27fca8 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
@@ -62,17 +62,16 @@ def get_gantt_data(
     dag_id: str,
     run_id: str,
     session: SessionDep,
-    start_date: Annotated[datetime | None, Query()] = None,
-    end_date: Annotated[datetime | None, Query()] = None,
+    start_date_range: Annotated[datetime | None, Query(alias="start_date")] = None,
+    end_date_range: Annotated[datetime | None, Query(alias="end_date")] = None,
 ) -> GanttResponse:
-   
     """Get all task instance tries for Gantt chart."""
-    
-    if start_date and end_date and start_date > end_date:
+    if start_date_range and end_date_range and start_date_range > end_date_range:
         raise HTTPException(
             status.HTTP_400_BAD_REQUEST,
             "start_date cannot be greater than end_date",
         )
+
     # Exclude mapped tasks (use grid summaries) and UP_FOR_RETRY (already in history)
     current_tis = select(
         TaskInstance.task_id.label("task_id"),
@@ -87,13 +86,13 @@ def get_gantt_data(
         TaskInstance.map_index == -1,
         or_(TaskInstance.state != TaskInstanceState.UP_FOR_RETRY, TaskInstance.state.is_(None)),
         *(
-            [or_(TaskInstance.end_date >= start_date, TaskInstance.end_date.is_(None))]
-            if start_date is not None
+            [or_(TaskInstance.end_date >= start_date_range, TaskInstance.end_date.is_(None))]
+            if start_date_range is not None
             else []
         ),
         *(
-            [TaskInstance.start_date <= end_date]
-            if end_date is not None
+            [TaskInstance.start_date <= end_date_range]
+            if end_date_range is not None
             else []
         ),
     )
@@ -110,13 +109,13 @@ def get_gantt_data(
         TaskInstanceHistory.run_id == run_id,
         TaskInstanceHistory.map_index == -1,
         *(
-            [or_(TaskInstanceHistory.end_date >= start_date, TaskInstanceHistory.end_date.is_(None))]
-            if start_date is not None
+            [or_(TaskInstanceHistory.end_date >= start_date_range, TaskInstanceHistory.end_date.is_(None))]
+            if start_date_range is not None
             else []
         ),
         *(
-            [TaskInstanceHistory.start_date <= end_date]
-            if end_date is not None
+            [TaskInstanceHistory.start_date <= end_date_range]
+            if end_date_range is not None
             else []
         ),
     )
@@ -129,7 +128,7 @@ def get_gantt_data(
     if not results:
         raise HTTPException(
             status.HTTP_404_NOT_FOUND,
-            f"No task instances for dag_id={dag_id} run_id={run_id}",
+            detail="DAG or DagRun not found",
         )
 
     task_instances = [
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
index e44f9cc3856e1..624d0e70c900a 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
@@ -96,6 +96,7 @@
   "duration": "Duration",
   "edit": "Edit",
   "endDate": "End Date",
+  "endDateBeforeStartDate": "End date cannot be before start date",
   "error": {
     "back": "Back",
     "defaultMessage": "An unexpected error occurred",
@@ -212,6 +213,7 @@
   "sourceAssetEvent_one": "Source Asset Event",
   "sourceAssetEvent_other": "Source Asset Events",
   "startDate": "Start Date",
+  "startDateAfterEndDate": "Start date cannot be after end date",
   "state": "State",
   "states": {
     "deferred": "Deferred",
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
index b597019b8c983..492bd274b44fb 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Input, Text, useToken } from "@chakra-ui/react";
+import { Box, Field, Input, useToken } from "@chakra-ui/react";
 import {
   Chart as ChartJS,
   CategoryScale,
@@ -85,6 +85,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams();
   const [filterStartDate, setFilterStartDate] = useState("");
   const [filterEndDate, setFilterEndDate] = useState("");
+  const [dateError, setDateError] = useState("");
 
   const [searchParams] = useSearchParams();
   const { openGroupIds } = useOpenGroups();
@@ -146,7 +147,6 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   const gridTiSummaries = summariesByRunId.get(runId);
   const summariesLoading = Boolean(runId && selectedRun && !summariesByRunId.has(runId));
 
-  // Single fetch for all Gantt data (individual task tries)
   // startDate and endDate are sent to the backend as query parameters.
   // The server filters the data — NOT the browser.
   const { data: ganttData, isLoading: ganttLoading } = useGanttServiceGetGanttData(
@@ -239,22 +239,23 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
 
   return (
     <>
-      {/* Date inputs that trigger a new API fetch when changed */}
-      <Box mb="4" display="flex" gap="4" alignItems="flex-end">
-        <Box display="flex" flexDirection="column">
-          <Text color="fg.muted" fontSize="xs" mb={1}>
+      {/* Date range inputs — values are sent to backend as query params, no client-side filtering */}
+      <Box alignItems="flex-start" display="flex" gap="4" mb="4">
+        <Field.Root invalid={Boolean(dateError)} maxW="200px">
+          <Field.Label color="fg.muted" fontSize="xs">
             {translate("startDate")}
-          </Text>
+          </Field.Label>
           <Input
             fontSize="sm"
             fontWeight="medium"
-            maxW="200px"
             onChange={(e) => {
               const value = e.target.value;
-              if (filterEndDate && value > filterEndDate) {
-                alert("Start date cannot be after end date");
+
+              if (value && filterEndDate && value > filterEndDate) {
+                setDateError(translate("startDateAfterEndDate"));
                 return;
               }
+              setDateError("");
               setFilterStartDate(value);
             }}
             placeholder="YYYY-MM-DD"
@@ -262,21 +263,23 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
             type="date"
             value={filterStartDate}
           />
-        </Box>
-        <Box display="flex" flexDirection="column">
-          <Text color="fg.muted" fontSize="xs" mb={1}>
+        </Field.Root>
+
+        <Field.Root invalid={Boolean(dateError)} maxW="200px">
+          <Field.Label color="fg.muted" fontSize="xs">
             {translate("endDate")}
-          </Text>
+          </Field.Label>
           <Input
             fontSize="sm"
             fontWeight="medium"
-            maxW="200px"
             onChange={(e) => {
               const value = e.target.value;
-              if (filterStartDate && value < filterStartDate) {
-                alert("End date cannot be before start date");
+
+              if (value && filterStartDate && value < filterStartDate) {
+                setDateError(translate("endDateBeforeStartDate"));
                 return;
               }
+              setDateError("");
               setFilterEndDate(value);
             }}
             placeholder="YYYY-MM-DD"
@@ -284,7 +287,8 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
             type="date"
             value={filterEndDate}
           />
-        </Box>
+          {dateError ? <Field.ErrorText fontSize="xs">{dateError}</Field.ErrorText> : undefined}
+        </Field.Root>
       </Box>
       <Box
         height={`${fixedHeight}px`}
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
index 0e942efacf127..79659bd652302 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
@@ -328,25 +328,63 @@ def test_should_response_404(self, test_client, dag_id, run_id):
         assert response.status_code == 404
 
     def test_gantt_with_start_date(self, test_client):
+        """Filtering by start_date excludes tasks that ended before the filter."""
+        # Get all tasks without filter
+        unfiltered = test_client.get(f"/gantt/{DAG_ID}/run_1")
+        assert unfiltered.status_code == 200
+        unfiltered_ids = [ti["task_id"] for ti in unfiltered.json()["task_instances"]]
+
+        # Filter: only tasks with end_date >= 10:06 (or NULL end_date for running tasks)
+        # task  ends at 10:05 → excluded
+        # task2 ends at 10:10 → included
+        # task3 has NULL end_date (running) → always included
         response = test_client.get(
             f"/gantt/{DAG_ID}/run_1",
-            params={"start_date": "2024-11-30T10:06:00"},
+            params={"start_date": "2024-11-30T10:06:00Z"},
         )
         assert response.status_code == 200
 
         data = response.json()
-
-        # running task (NULL end_date) should STILL be included
         task_ids = [ti["task_id"] for ti in data["task_instances"]]
+
+        # running task (NULL end_date) must always be included
         assert "task3" in task_ids
+        # task2 ended after the filter start → included
+        assert "task2" in task_ids
+        # task ended before the filter start → excluded
+        assert "task" not in task_ids
+        # filtered result must have fewer tasks than unfiltered
+        assert len(task_ids) < len(unfiltered_ids)
+
+    def test_gantt_with_end_date(self, test_client):
+        """Filtering by end_date excludes tasks that started after the filter."""
+        # Filter: only tasks with start_date <= 10:04
+        # task  starts at 10:00 → included
+        # task2 starts at 10:05 → excluded
+        # task3 starts at 10:10 → excluded
+        response = test_client.get(
+            f"/gantt/{DAG_ID}/run_1",
+            params={"end_date": "2024-11-30T10:04:00Z"},
+        )
+        assert response.status_code == 200
+
+        data = response.json()
+        task_ids = [ti["task_id"] for ti in data["task_instances"]]
 
+        # task started before end_date → included
+        assert "task" in task_ids
+        # task3 started after end_date → excluded
+        assert "task3" not in task_ids
+        # task2 started after end_date → excluded
+        assert "task2" not in task_ids
 
-    def test_gantt_invalid_date_range(self, test_client):
+    def test_invalid_date_range_returns_400(self, test_client):
+        """start_date after end_date must return 400."""
         response = test_client.get(
             f"/gantt/{DAG_ID}/run_1",
             params={
-                "start_date": "2024-12-01T00:00:00",
-                "end_date": "2024-11-01T00:00:00",
+                "start_date": "2024-12-01T00:00:00Z",
+                "end_date": "2024-11-01T00:00:00Z",
             },
         )
         assert response.status_code == 400
\ No newline at end of file
diff --git a/newsfragments/64387.significant.rst b/newsfragments/64387.significant.rst
deleted file mode 100644
index a2a2b2d4acf8c..0000000000000
--- a/newsfragments/64387.significant.rst
+++ /dev/null
@@ -1 +0,0 @@
-Add start date and end date filter to the Gantt view with server-side filtering.

From df15b673928cec74f05dae33baee6342298c6c7d Mon Sep 17 00:00:00 2001
From: Smitaambiger <your-email@gmail.com>
Date: Wed, 8 Apr 2026 18:32:57 +0000
Subject: [PATCH 7/7] fix: restore start_date filter and handle NULL start_date
 for end_date filter

---
 airflow-core/package-lock.json                        |  6 ++++++
 .../airflow/api_fastapi/core_api/routes/ui/gantt.py   | 11 ++++++++---
 .../airflow/ui/src/layouts/Details/Gantt/Gantt.tsx    | 10 +++-------
 .../unit/api_fastapi/core_api/routes/ui/test_gantt.py |  2 +-
 scripts/ci/prek/check_excluded_provider_markers.py    |  0
 .../ci/prek/check_metrics_synced_with_the_registry.py |  0
 scripts/ci/prek/check_registry_types_json_sync.py     |  0
 7 files changed, 18 insertions(+), 11 deletions(-)
 create mode 100644 airflow-core/package-lock.json
 mode change 100644 => 100755 scripts/ci/prek/check_excluded_provider_markers.py
 mode change 100644 => 100755 scripts/ci/prek/check_metrics_synced_with_the_registry.py
 mode change 100644 => 100755 scripts/ci/prek/check_registry_types_json_sync.py

diff --git a/airflow-core/package-lock.json b/airflow-core/package-lock.json
new file mode 100644
index 0000000000000..47020b70c4680
--- /dev/null
+++ b/airflow-core/package-lock.json
@@ -0,0 +1,6 @@
+{
+  "name": "airflow-core",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
index 088e99c27fca8..2207370e0965a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py
@@ -91,7 +91,7 @@ def get_gantt_data(
             else []
         ),
         *(
-            [TaskInstance.start_date <= end_date_range]
+            [or_(TaskInstance.start_date <= end_date_range, TaskInstance.start_date.is_(None))]
             if end_date_range is not None
             else []
         ),
@@ -114,7 +114,12 @@ def get_gantt_data(
             else []
         ),
         *(
-            [TaskInstanceHistory.start_date <= end_date_range]
+            [
+                or_(
+                    TaskInstanceHistory.start_date <= end_date_range,
+                    TaskInstanceHistory.start_date.is_(None),
+                )
+            ]
             if end_date_range is not None
             else []
         ),
@@ -143,4 +148,4 @@ def get_gantt_data(
         for row in results
     ]
 
-    return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances)
\ No newline at end of file
+    return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances)
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
index 492bd274b44fb..f994020c5d92a 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
@@ -71,8 +71,6 @@ ChartJS.register(
 type Props = {
   readonly dagRunState?: DagRunState | undefined;
   readonly limit: number;
-  readonly runAfterGte?: string | undefined;
-  readonly runAfterLte?: string | undefined;
   readonly runType?: DagRunType | undefined;
   readonly triggeringUser?: string | undefined;
 };
@@ -81,7 +79,7 @@ const CHART_PADDING = 36;
 const CHART_ROW_HEIGHT = 20;
 const MIN_BAR_WIDTH = 10;
 
-export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, triggeringUser }: Props) => {
+export const Gantt = ({ dagRunState, limit, runType, triggeringUser }: Props) => {
   const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams();
   const [filterStartDate, setFilterStartDate] = useState("");
   const [filterEndDate, setFilterEndDate] = useState("");
@@ -120,8 +118,6 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
   const { data: gridRuns, isLoading: runsLoading } = useGridRuns({
     dagRunState,
     limit,
-    runAfterGte,
-    runAfterLte,
     runType,
     triggeringUser,
   });
@@ -222,7 +218,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
     translate,
   });
 
-  if (runId === "" || (!isLoading && !selectedRun)) {
+  if (runId === "") {
     return undefined;
   }
 
@@ -308,4 +304,4 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
       </Box>
     </>
   );
-};
\ No newline at end of file
+};
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
index 79659bd652302..ade423c7fb73b 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py
@@ -387,4 +387,4 @@ def test_invalid_date_range_returns_400(self, test_client):
                 "end_date": "2024-11-01T00:00:00Z",
             },
         )
-        assert response.status_code == 400
\ No newline at end of file
+        assert response.status_code == 400
diff --git a/scripts/ci/prek/check_excluded_provider_markers.py b/scripts/ci/prek/check_excluded_provider_markers.py
old mode 100644
new mode 100755
diff --git a/scripts/ci/prek/check_metrics_synced_with_the_registry.py b/scripts/ci/prek/check_metrics_synced_with_the_registry.py
old mode 100644
new mode 100755
diff --git a/scripts/ci/prek/check_registry_types_json_sync.py b/scripts/ci/prek/check_registry_types_json_sync.py
old mode 100644
new mode 100755
