diff --git a/package-lock.json b/package-lock.json
index 9161c4e2..9d146221 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
         "@radix-ui/react-slot": "^1.1.0",
         "@radix-ui/react-switch": "^1.1.2",
         "@radix-ui/react-tooltip": "^1.1.6",
+        "@tanstack/react-query": "^5.64.1",
         "@types/prismjs": "^1.26.5",
         "@types/react-syntax-highlighter": "^15.5.13",
         "class-variance-authority": "^0.7.1",
@@ -2725,6 +2726,32 @@
         "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
       }
     },
+    "node_modules/@tanstack/query-core": {
+      "version": "5.64.1",
+      "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz",
+      "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
+    "node_modules/@tanstack/react-query": {
+      "version": "5.64.1",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz",
+      "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==",
+      "license": "MIT",
+      "dependencies": {
+        "@tanstack/query-core": "5.64.1"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": "^18 || ^19"
+      }
+    },
     "node_modules/@testing-library/dom": {
       "version": "10.4.0",
       "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
diff --git a/package.json b/package.json
index 027080d1..877163df 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
     "@radix-ui/react-slot": "^1.1.0",
     "@radix-ui/react-switch": "^1.1.2",
     "@radix-ui/react-tooltip": "^1.1.6",
+    "@tanstack/react-query": "^5.64.1",
     "@types/prismjs": "^1.26.5",
     "@types/react-syntax-highlighter": "^15.5.13",
     "class-variance-authority": "^0.7.1",
@@ -81,4 +82,4 @@
   "overrides": {
     "vite": "^6.0.1"
   }
-}
\ No newline at end of file
+}
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
index ba200ee7..938a2fac 100644
--- a/src/components/Dashboard.tsx
+++ b/src/components/Dashboard.tsx
@@ -21,6 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
 import { useSearchParams } from "react-router-dom";
 import { AlertConversation } from "@/api/generated";
 import { getMaliciousPackage } from "@/lib/utils";
+import { CardCodegateStatus } from "@/features/dashboard/components/card-codegate-status";
 
 const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => {
   const data = getMaliciousPackage(input);
@@ -127,16 +128,11 @@ export function Dashboard() {
 
   return (
     <div className="flex-col">
-      <div className="flex flex-wrap items-center gap-4 w-full">
-        <div className="min-w-80 w-1/3 h-60">
-          <BarChart data={alerts} loading={loading} />
-        </div>
-        <div className="min-w-80 w-1/4 h-60">
-          <PieChart data={maliciousPackages} loading={loading} />
-        </div>
-        <div className="relative w-[370px] h-60">
-          <LineChart data={alerts} loading={loading} />
-        </div>
+      <div className="grid 2xl:grid-cols-4 sm:grid-cols-2 grid-cols-1 items-stretch gap-4 w-full">
+        <CardCodegateStatus />
+        <BarChart data={alerts} loading={loading} />
+        <PieChart data={maliciousPackages} loading={loading} />
+        <LineChart data={alerts} loading={loading} />
       </div>
 
       <Separator className="my-8" />
@@ -193,7 +189,7 @@ export function Dashboard() {
         </div>
       </div>
       <div className="overflow-x-auto">
-        <Table>
+        <Table data-testid="alerts-table">
           <TableHeader>
             <TableRow>
               <TableHead className="w-[150px]">Trigger Type</TableHead>
diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx
index 456100d9..1be53c1a 100644
--- a/src/components/__tests__/Dashboard.test.tsx
+++ b/src/components/__tests__/Dashboard.test.tsx
@@ -154,19 +154,21 @@ describe("Dashboard", () => {
     ).toBeVisible();
     expect(screen.getByRole("searchbox")).toBeVisible();
 
-    const row = screen.getAllByRole("row")[1] as HTMLElement;
+    const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole(
+      "row",
+    )[1] as HTMLElement;
+    const secondRow = within(screen.getByTestId("alerts-table")).getAllByRole(
+      "row",
+    )[2] as HTMLElement;
 
-    expect(within(row).getByText(/ghp_token/i)).toBeVisible();
-    expect(within(row).getByText(/codegate-secrets/i)).toBeVisible();
-    expect(within(row).getAllByText(/n\/a/i).length).toEqual(2);
-    expect(within(row).getByText(/2025\/01\/07/i)).toBeVisible();
-    expect(within(row).getByTestId(/time/i)).toBeVisible();
+    expect(within(firstRow).getByText(/ghp_token/i)).toBeVisible();
+    expect(within(firstRow).getByText(/codegate-secrets/i)).toBeVisible();
+    expect(within(firstRow).getAllByText(/n\/a/i).length).toEqual(2);
+    expect(within(firstRow).getByText(/2025\/01\/07/i)).toBeVisible();
+    expect(within(firstRow).getByTestId(/time/i)).toBeVisible();
 
     // check trigger_string null
-    expect(
-      within(screen.getAllByRole("row")[2] as HTMLElement).getAllByText(/n\/a/i)
-        .length,
-    ).toEqual(3);
+    expect(within(secondRow).getAllByText(/n\/a/i).length).toEqual(3);
   });
 
   it("should render malicious pkg", async () => {
@@ -271,7 +273,9 @@ describe("Dashboard", () => {
     await waitFor(() =>
       expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("1"),
     );
-    const row = screen.getAllByRole("row")[1] as HTMLElement;
+    const row = within(screen.getByTestId("alerts-table")).getAllByRole(
+      "row",
+    )[1] as HTMLElement;
     expect(within(row).getByText(/ghp_token/i)).toBeVisible();
     expect(within(row).getByText(/codegate-secrets/i)).toBeVisible();
   });
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
index 754ad93a..0b46be9e 100644
--- a/src/components/ui/card.tsx
+++ b/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 const Card = React.forwardRef<
   HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
     ref={ref}
     className={cn(
       "rounded-lg border bg-card text-card-foreground shadow-sm",
-      className
+      className,
     )}
     {...props}
   />
-))
-Card.displayName = "Card"
+));
+Card.displayName = "Card";
 
 const CardHeader = React.forwardRef<
   HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
     className={cn("flex flex-col space-y-1 p-4", className)}
     {...props}
   />
-))
-CardHeader.displayName = "CardHeader"
+));
+CardHeader.displayName = "CardHeader";
 
 const CardTitle = React.forwardRef<
   HTMLDivElement,
@@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
     ref={ref}
     className={cn(
       "text-xl font-semibold leading-none tracking-tight",
-      className
+      className,
     )}
     {...props}
   />
-))
-CardTitle.displayName = "CardTitle"
+));
+CardTitle.displayName = "CardTitle";
 
 const CardDescription = React.forwardRef<
   HTMLDivElement,
@@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
     className={cn("text-sm text-muted-foreground", className)}
     {...props}
   />
-))
-CardDescription.displayName = "CardDescription"
+));
+CardDescription.displayName = "CardDescription";
 
 const CardContent = React.forwardRef<
   HTMLDivElement,
   React.HTMLAttributes<HTMLDivElement>
 >(({ className, ...props }, ref) => (
   <div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
-))
-CardContent.displayName = "CardContent"
+));
+CardContent.displayName = "CardContent";
 
 const CardFooter = React.forwardRef<
   HTMLDivElement,
@@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
     className={cn("flex items-center p-4 pt-0", className)}
     {...props}
   />
-))
-CardFooter.displayName = "CardFooter"
+));
+CardFooter.displayName = "CardFooter";
 
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+export {
+  Card,
+  CardHeader,
+  CardFooter,
+  CardTitle,
+  CardDescription,
+  CardContent,
+};
diff --git a/src/features/dashboard/components/__tests__/card-codegate-status.test.tsx b/src/features/dashboard/components/__tests__/card-codegate-status.test.tsx
new file mode 100644
index 00000000..838cc424
--- /dev/null
+++ b/src/features/dashboard/components/__tests__/card-codegate-status.test.tsx
@@ -0,0 +1,50 @@
+import { server } from "@/mocks/msw/node";
+import { http, HttpResponse } from "msw";
+import { expect } from "vitest";
+import { CardCodegateStatus } from "../card-codegate-status";
+import { render, waitFor } from "@/lib/test-utils";
+
+const renderComponent = () => render(<CardCodegateStatus />);
+
+describe("CardCodegateStatus", () => {
+  test("renders 'healthy' state", async () => {
+    server.use(
+      http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
+    );
+
+    const { getByText } = renderComponent();
+
+    await waitFor(
+      () => {
+        expect(getByText(/healthy/i)).toBeVisible();
+      },
+      { timeout: 10_000 },
+    );
+  });
+
+  test("renders 'unhealthy' state", async () => {
+    server.use(http.get("*/health", () => HttpResponse.json({ status: null })));
+
+    const { getByText } = renderComponent();
+
+    await waitFor(
+      () => {
+        expect(getByText(/unhealthy/i)).toBeVisible();
+      },
+      { timeout: 10_000 },
+    );
+  });
+
+  test("renders 'error' state", async () => {
+    server.use(http.get("*/health", () => HttpResponse.error()));
+
+    const { getByText } = renderComponent();
+
+    await waitFor(
+      () => {
+        expect(getByText(/an error occurred/i)).toBeVisible();
+      },
+      { timeout: 10_000 },
+    );
+  });
+});
diff --git a/src/features/dashboard/components/card-codegate-status.tsx b/src/features/dashboard/components/card-codegate-status.tsx
new file mode 100644
index 00000000..82d12779
--- /dev/null
+++ b/src/features/dashboard/components/card-codegate-status.tsx
@@ -0,0 +1,236 @@
+import {
+  Card,
+  CardContent,
+  CardFooter,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import { TableRow, TableBody, TableCell, Table } from "@/components/ui/table";
+import { cn } from "@/lib/utils";
+import { useQuery } from "@tanstack/react-query";
+import { format } from "date-fns";
+import {
+  CheckCircle2,
+  LoaderCircle,
+  XCircle,
+  ChevronDown,
+  Check,
+} from "lucide-react";
+import { Dispatch, SetStateAction, useState } from "react";
+
+const INTERVAL = {
+  "1_SEC": { value: 1_000, name: "1 second" },
+  "5_SEC": { value: 5_000, name: "5 seconds" },
+  "10_SEC": { value: 10_000, name: "10 seconds" },
+  "30_SEC": { value: 30_000, name: "30 seconds" },
+  "1_MIN": { value: 60_000, name: "1 minute" },
+  "5_MIN": { value: 300_000, name: "5 minutes" },
+  "10_MIN": { value: 600_000, name: "10 minutes" },
+} as const;
+
+const DEFAULT_INTERVAL: Interval = "5_SEC";
+
+type Interval = keyof typeof INTERVAL;
+
+enum Status {
+  HEALTHY = "Healthy",
+  UNHEALTHY = "Unhealthy",
+}
+
+type HealthResp = { status: "healthy" | unknown } | null;
+
+const getStatus = async (): Promise<Status | null> => {
+  const resp = await fetch(
+    new URL("/health", import.meta.env.VITE_BASE_API_URL),
+  );
+  const data = (await resp.json()) as unknown as HealthResp;
+
+  if (data?.status === "healthy") return Status.HEALTHY;
+  if (data?.status !== "healthy") return Status.UNHEALTHY;
+
+  return null;
+};
+
+const useStatus = (pollingInterval: Interval) =>
+  useQuery({
+    queryFn: getStatus,
+    queryKey: ["getStatus", { pollingInterval }],
+    refetchInterval: INTERVAL[pollingInterval].value,
+    staleTime: Infinity,
+    gcTime: Infinity,
+    refetchIntervalInBackground: true,
+    refetchOnMount: true,
+    refetchOnReconnect: true,
+    refetchOnWindowFocus: true,
+    retry: false,
+  });
+
+const StatusText = ({
+  status,
+  isPending,
+}: {
+  status: Status | null;
+  isPending: boolean;
+}) => {
+  if (isPending || status === null) {
+    return (
+      <div className="flex gap-2 items-center text-gray-600 justify-end overflow-hidden">
+        Checking <LoaderCircle className="size-4 animate-spin" />
+      </div>
+    );
+  }
+
+  switch (status) {
+    case Status.HEALTHY:
+      return (
+        <div className="flex gap-2 items-center text-green-600 justify-end">
+          {Status.HEALTHY} <CheckCircle2 className="size-4" />
+        </div>
+      );
+    case Status.UNHEALTHY:
+      return (
+        <div className="flex gap-2 items-center text-red-600 justify-end overflow-hidden">
+          {Status.UNHEALTHY} <XCircle className="size-4" />
+        </div>
+      );
+    default: {
+      status satisfies never;
+    }
+  }
+};
+
+function ErrorUI() {
+  return (
+    <div className="flex flex-col items-center justify-center py-8">
+      <XCircle className="text-red-600 mb-2 size-8" />
+      <div className="text-md font-semibold text-gray-600 text-center">
+        An error occurred
+      </div>
+      <div className="text-sm text-gray-600 text-center text-balance">
+        If this issue persists, please reach out to us on{" "}
+        <a
+          className="underline text-gray-700"
+          href="https://discord.gg/stacklok"
+          rel="noopener noreferrer"
+          target="_blank"
+        >
+          Discord
+        </a>{" "}
+        or open a new{" "}
+        <a
+          className="underline text-gray-700"
+          href="https://github.com/stacklok/codegate/issues/new"
+          rel="noopener noreferrer"
+          target="_blank"
+        >
+          Github issue
+        </a>
+      </div>
+    </div>
+  );
+}
+
+function PollIntervalControl({
+  className,
+  pollingInterval,
+  setPollingInterval,
+}: {
+  className?: string;
+  pollingInterval: Interval;
+  setPollingInterval: Dispatch<SetStateAction<Interval>>;
+}) {
+  return (
+    <div className={cn("flex items-center relative group", className)}>
+      <div className="text-gray-600 hover:text-gray-800 font-normal cursor-pointer text-base px-2 py-1 rounded-md hover:bg-blue-50 transition-colors flex gap-1 items-center">
+        <div>
+          <div className="text-sm font-semibold text-gray-500 text-right">
+            Check for updates
+          </div>
+          <div className="text-sm text-gray-500 text-right">
+            every {INTERVAL[pollingInterval].name}
+          </div>
+        </div>
+        <ChevronDown className="size-4" />
+      </div>
+      <div className="p-1 absolute right-0 top-full mt-2 w-32 bg-white rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 border border-gray-100">
+        {Object.entries(INTERVAL).map(([key, { name }]) => {
+          const isActive = key === pollingInterval;
+
+          return (
+            <button
+              onClick={() => setPollingInterval(key as Interval)}
+              data-active={isActive}
+              className="text-right :not:last:mb-1 font-normal text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-0.5 hover:text-gray-800 &[data-active=true]:text-gray-800 flex items-center justify-between w-full"
+              key={key}
+            >
+              {name}
+              {isActive ? <Check className="size-3" /> : null}
+            </button>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
+
+export function InnerContent({
+  isError,
+  isPending,
+  data,
+}: Pick<ReturnType<typeof useStatus>, "data" | "isPending" | "isError">) {
+  if (!isPending && isError) {
+    return <ErrorUI />;
+  }
+
+  return (
+    <Table className="h-max">
+      <TableBody>
+        <TableRow className="hover:bg-transparent">
+          <TableCell className="pl-0">CodeGate server</TableCell>
+          <TableCell className="pr-0 text-end">
+            <StatusText isPending={isPending} status={data ?? null} />
+          </TableCell>
+        </TableRow>
+      </TableBody>
+    </Table>
+  );
+}
+
+export function CardCodegateStatus() {
+  const [pollingInterval, setPollingInterval] = useState<Interval>(
+    () => DEFAULT_INTERVAL,
+  );
+  const { data, dataUpdatedAt, isPending, isError } =
+    useStatus(pollingInterval);
+
+  return (
+    <Card className="h-full flex flex-col">
+      <CardHeader>
+        <CardTitle className="flex justify-between items-center">
+          <span className="block">CodeGate Status</span>
+        </CardTitle>
+      </CardHeader>
+
+      <CardContent className="h-max">
+        <InnerContent data={data} isPending={isPending} isError={isError} />
+      </CardContent>
+
+      <CardFooter className="border-t border-gray-200 mt-auto py-2 pr-2">
+        <div>
+          <div className="text-sm font-semibold text-gray-500">
+            Last checked
+          </div>
+          <div className="text-sm text-gray-500">
+            {format(new Date(dataUpdatedAt), "pp")}
+          </div>
+        </div>
+
+        <PollIntervalControl
+          className="ml-auto"
+          pollingInterval={pollingInterval}
+          setPollingInterval={setPollingInterval}
+        />
+      </CardFooter>
+    </Card>
+  );
+}
diff --git a/src/lib/test-utils.tsx b/src/lib/test-utils.tsx
index c1572da7..f6880dbf 100644
--- a/src/lib/test-utils.tsx
+++ b/src/lib/test-utils.tsx
@@ -1,4 +1,5 @@
 import { SidebarProvider } from "@/components/ui/sidebar";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { RenderOptions, render } from "@testing-library/react";
 import React from "react";
 import {
@@ -18,14 +19,27 @@ const renderWithProviders = (
   options?: Omit<RenderOptions, "queries"> & RoutConfig,
 ) =>
   render(
-    <MemoryRouter {...options?.routeConfig}>
-      <Routes>
-        <Route
-          path={options?.pathConfig ?? "*"}
-          element={<SidebarProvider>{children}</SidebarProvider>}
-        />
-      </Routes>
-    </MemoryRouter>,
+    <QueryClientProvider
+      client={
+        new QueryClient({
+          defaultOptions: {
+            queries: {
+              gcTime: 0,
+              staleTime: 0,
+            },
+          },
+        })
+      }
+    >
+      <MemoryRouter {...options?.routeConfig}>
+        <Routes>
+          <Route
+            path={options?.pathConfig ?? "*"}
+            element={<SidebarProvider>{children}</SidebarProvider>}
+          />
+        </Routes>
+      </MemoryRouter>
+    </QueryClientProvider>,
   );
 
 export * from "@testing-library/react";
diff --git a/src/main.tsx b/src/main.tsx
index 214fd637..03661b48 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -4,6 +4,7 @@ import "./index.css";
 import App from "./App.tsx";
 import { BrowserRouter } from "react-router-dom";
 import { SidebarProvider } from "./components/ui/sidebar.tsx";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import ErrorBoundary from "./components/ErrorBoundary.tsx";
 import { Error } from "./components/Error.tsx";
 
@@ -12,7 +13,9 @@ createRoot(document.getElementById("root")!).render(
     <BrowserRouter>
       <SidebarProvider>
         <ErrorBoundary fallback={<Error />}>
-          <App />
+          <QueryClientProvider client={new QueryClient()}>
+            <App />
+          </QueryClientProvider>
         </ErrorBoundary>
       </SidebarProvider>
     </BrowserRouter>
diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts
index f52db2b0..a2277a5a 100644
--- a/src/mocks/msw/handlers.ts
+++ b/src/mocks/msw/handlers.ts
@@ -3,6 +3,7 @@ import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json";
 import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json";
 
 export const handlers = [
+  http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
   http.get("*/dashboard/messages", () => {
     return HttpResponse.json(mockedPrompts);
   }),
diff --git a/src/viz/LineChart.tsx b/src/viz/LineChart.tsx
index 0edc5c79..84a52693 100644
--- a/src/viz/LineChart.tsx
+++ b/src/viz/LineChart.tsx
@@ -85,8 +85,8 @@ export function LineChart({
       <CardHeader>
         <CardTitle>Alerts by date</CardTitle>
       </CardHeader>
-      <CardContent className="h-full">
-        <ChartContainer config={chartConfig}>
+      <CardContent>
+        <ChartContainer config={chartConfig} className="min-h-[10rem]">
           <LineChartsUI
             accessibilityLayer
             data={chartData}