diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
index d41aa8d9..8b54337e 100644
--- a/src/components/Dashboard.tsx
+++ b/src/components/Dashboard.tsx
@@ -23,7 +23,7 @@ import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit";
 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";
+import { CodegateStatus } from "@/features/dashboard-codegate-status/components/codegate-status";
 import { Search } from "lucide-react";
 import {
   useAlertsData,
@@ -132,7 +132,7 @@ export function Dashboard() {
   return (
     <div className="flex-col">
       <div className="grid 2xl:grid-cols-4 sm:grid-cols-2 grid-cols-1 items-stretch gap-4 w-full">
-        <CardCodegateStatus />
+        <CodegateStatus />
         <BarChart data={alerts} loading={isLoading} />
         <PieChart data={maliciousPackages} loading={isLoading} />
         <LineChart data={alerts} loading={isLoading} />
diff --git a/src/features/dashboard-codegate-status/components/__tests__/codegate-status.test.tsx b/src/features/dashboard-codegate-status/components/__tests__/codegate-status.test.tsx
new file mode 100644
index 00000000..d4704b8d
--- /dev/null
+++ b/src/features/dashboard-codegate-status/components/__tests__/codegate-status.test.tsx
@@ -0,0 +1,134 @@
+import { server } from "@/mocks/msw/node";
+import { http, HttpResponse } from "msw";
+import { expect } from "vitest";
+import { CodegateStatus } from "../codegate-status";
+import { render, waitFor } from "@/lib/test-utils";
+
+const renderComponent = () => render(<CodegateStatus />);
+
+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 when health check request fails", async () => {
+    server.use(http.get("*/health", () => HttpResponse.error()));
+
+    const { getByText } = renderComponent();
+
+    await waitFor(
+      () => {
+        expect(getByText(/an error occurred/i)).toBeVisible();
+      },
+      { timeout: 10_000 },
+    );
+  });
+
+  test("renders 'error' state when version check request fails", async () => {
+    server.use(http.get("*/dashboard/version", () => HttpResponse.error()));
+
+    const { getByText } = renderComponent();
+
+    await waitFor(
+      () => {
+        expect(getByText(/an error occurred/i)).toBeVisible();
+      },
+      { timeout: 10_000 },
+    );
+  });
+
+  test("renders 'latest version' state", async () => {
+    server.use(
+      http.get("*/dashboard/version", () =>
+        HttpResponse.json({
+          current_version: "foo",
+          latest_version: "foo",
+          is_latest: true,
+          error: null,
+        }),
+      ),
+    );
+
+    const { getByText } = renderComponent();
+
+    await waitFor(
+      () => {
+        expect(getByText(/latest/i)).toBeVisible();
+      },
+      { timeout: 10_000 },
+    );
+  });
+
+  test("renders 'update available' state", async () => {
+    server.use(
+      http.get("*/dashboard/version", () =>
+        HttpResponse.json({
+          current_version: "foo",
+          latest_version: "bar",
+          is_latest: false,
+          error: null,
+        }),
+      ),
+    );
+
+    const { getByRole } = renderComponent();
+
+    await waitFor(
+      () => {
+        const role = getByRole("link", { name: /update available/i });
+        expect(role).toBeVisible();
+        expect(role).toHaveAttribute(
+          "href",
+          "https://docs.codegate.ai/how-to/install#upgrade-codegate",
+        );
+      },
+      { timeout: 10_000 },
+    );
+  });
+
+  test("renders 'version check error' state", async () => {
+    server.use(
+      http.get("*/dashboard/version", () =>
+        HttpResponse.json({
+          current_version: "foo",
+          latest_version: "bar",
+          is_latest: false,
+          error: "foo",
+        }),
+      ),
+    );
+
+    const { getByText } = renderComponent();
+
+    await waitFor(
+      () => {
+        expect(getByText(/error checking version/i)).toBeVisible();
+      },
+      { timeout: 10_000 },
+    );
+  });
+});
diff --git a/src/features/dashboard-codegate-status/components/codegate-status-error-ui.tsx b/src/features/dashboard-codegate-status/components/codegate-status-error-ui.tsx
new file mode 100644
index 00000000..bad6810a
--- /dev/null
+++ b/src/features/dashboard-codegate-status/components/codegate-status-error-ui.tsx
@@ -0,0 +1,32 @@
+import { XCircle } from "lucide-react";
+
+export function CodegateStatusErrorUI() {
+  return (
+    <div className="flex flex-col items-center justify-center py-8">
+      <XCircle className="text-red-600 mb-2 size-8" />
+      <div className="text-base font-semibold text-secondary text-center">
+        An error occurred
+      </div>
+      <div className="text-sm text-secondary text-center text-balance">
+        If this issue persists, please reach out to us on{" "}
+        <a
+          className="underline text-secondary"
+          href="https://discord.gg/stacklok"
+          rel="noopener noreferrer"
+          target="_blank"
+        >
+          Discord
+        </a>{" "}
+        or open a new{" "}
+        <a
+          className="underline text-secondary"
+          href="https://github.com/stacklok/codegate/issues/new"
+          rel="noopener noreferrer"
+          target="_blank"
+        >
+          Github issue
+        </a>
+      </div>
+    </div>
+  );
+}
diff --git a/src/features/dashboard-codegate-status/components/codegate-status-health.tsx b/src/features/dashboard-codegate-status/components/codegate-status-health.tsx
new file mode 100644
index 00000000..cbf0d99e
--- /dev/null
+++ b/src/features/dashboard-codegate-status/components/codegate-status-health.tsx
@@ -0,0 +1,36 @@
+import { LoaderCircle, CheckCircle2, XCircle } from "lucide-react";
+import { HealthStatus } from "../lib/get-codegate-health";
+
+export const CodegateStatusHealth = ({
+  data: data,
+  isPending,
+}: {
+  data: HealthStatus | null;
+  isPending: boolean;
+}) => {
+  if (isPending || data === null) {
+    return (
+      <div className="flex gap-2 items-center text-secondary justify-end overflow-hidden">
+        Checking <LoaderCircle className="size-4 shrink-0 animate-spin" />
+      </div>
+    );
+  }
+
+  switch (data) {
+    case HealthStatus.HEALTHY:
+      return (
+        <div className="flex gap-2 items-center text-primary justify-end">
+          {HealthStatus.HEALTHY} <CheckCircle2 className="size-4 shrink-0" />
+        </div>
+      );
+    case HealthStatus.UNHEALTHY:
+      return (
+        <div className="flex gap-2 items-center text-primary justify-end overflow-hidden">
+          {HealthStatus.UNHEALTHY} <XCircle className="size-4 shrink-0" />
+        </div>
+      );
+    default: {
+      data satisfies never;
+    }
+  }
+};
diff --git a/src/features/dashboard-codegate-status/components/codegate-status-polling-control.tsx b/src/features/dashboard-codegate-status/components/codegate-status-polling-control.tsx
new file mode 100644
index 00000000..b9e01e5c
--- /dev/null
+++ b/src/features/dashboard-codegate-status/components/codegate-status-polling-control.tsx
@@ -0,0 +1,54 @@
+import { Dispatch, SetStateAction } from "react";
+import {
+  Label,
+  Select,
+  SelectButton,
+  TDropdownItemOrSection,
+} from "@stacklok/ui-kit";
+
+// NOTE: We don't poll more than once per minute, as the server depends on
+// Github's public API, which is rate limited to 60reqs per hour.
+export const POLLING_INTERVAl = {
+  "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;
+
+export const INTERVAL_SELECT_ITEMS: TDropdownItemOrSection[] = Object.entries(
+  POLLING_INTERVAl,
+).map(([key, { name }]) => {
+  return { textValue: name, id: key };
+});
+
+export const DEFAULT_INTERVAL: PollingInterval = "5_MIN";
+
+export type PollingInterval = keyof typeof POLLING_INTERVAl;
+
+export function PollIntervalControl({
+  className,
+  pollingInterval,
+  setPollingInterval,
+}: {
+  className?: string;
+  pollingInterval: PollingInterval;
+  setPollingInterval: Dispatch<SetStateAction<PollingInterval>>;
+}) {
+  return (
+    <Select
+      className={className}
+      onSelectionChange={(v) =>
+        setPollingInterval(v.toString() as PollingInterval)
+      }
+      items={INTERVAL_SELECT_ITEMS}
+      defaultSelectedKey={pollingInterval}
+    >
+      <Label className="w-full text-right font-semibold text-secondary -mb-1">
+        Check for updates
+      </Label>
+      <SelectButton
+        isBorderless
+        className="h-7 max-w-36 pr-0 [&>span>span]:text-right [&>span>span]:justify-end !gap-0 text-secondary"
+      />
+    </Select>
+  );
+}
diff --git a/src/features/dashboard-codegate-status/components/codegate-status-refresh-button.tsx b/src/features/dashboard-codegate-status/components/codegate-status-refresh-button.tsx
new file mode 100644
index 00000000..ceb73746
--- /dev/null
+++ b/src/features/dashboard-codegate-status/components/codegate-status-refresh-button.tsx
@@ -0,0 +1,41 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { PollingInterval } from "./codegate-status-polling-control";
+import { getQueryOptionsCodeGateStatus } from "../hooks/use-codegate-status";
+import { useCallback, useEffect, useState } from "react";
+import { Button } from "@stacklok/ui-kit";
+import { RefreshCcw } from "lucide-react";
+import { twMerge } from "tailwind-merge";
+
+export function CodeGateStatusRefreshButton({
+  pollingInterval,
+  className,
+}: {
+  pollingInterval: PollingInterval;
+  className?: string;
+}) {
+  const queryClient = useQueryClient();
+  const { queryKey } = getQueryOptionsCodeGateStatus(pollingInterval);
+
+  const [refreshed, setRefreshed] = useState<boolean>(false);
+
+  useEffect(() => {
+    const id = setTimeout(() => setRefreshed(false), 500);
+    return () => clearTimeout(id);
+  }, [refreshed]);
+
+  const handleRefresh = useCallback(() => {
+    setRefreshed(true);
+    return queryClient.invalidateQueries({ queryKey, refetchType: "all" });
+  }, [queryClient, queryKey]);
+
+  return (
+    <Button
+      onPress={handleRefresh}
+      variant="tertiary"
+      className={twMerge("size-7", className)}
+      isDisabled={refreshed}
+    >
+      <RefreshCcw className={refreshed ? "animate-spin-once" : undefined} />
+    </Button>
+  );
+}
diff --git a/src/features/dashboard-codegate-status/components/codegate-status-version.tsx b/src/features/dashboard-codegate-status/components/codegate-status-version.tsx
new file mode 100644
index 00000000..65cbadfc
--- /dev/null
+++ b/src/features/dashboard-codegate-status/components/codegate-status-version.tsx
@@ -0,0 +1,61 @@
+import { LoaderCircle, CheckCircle2, CircleAlert, XCircle } from "lucide-react";
+import { VersionResponse } from "../lib/get-version-status";
+import { Link, Tooltip, TooltipTrigger } from "@stacklok/ui-kit";
+
+export const CodegateStatusVersion = ({
+  data,
+  isPending,
+}: {
+  data: VersionResponse | null;
+  isPending: boolean;
+}) => {
+  if (isPending || data === null) {
+    return (
+      <div className="flex gap-2 items-center text-secondary justify-end overflow-hidden">
+        Checking <LoaderCircle className="size-4 shrink-0 animate-spin" />
+      </div>
+    );
+  }
+
+  const { current_version, is_latest, latest_version, error } = data || {};
+
+  if (error !== null || is_latest === null) {
+    return (
+      <div className="flex gap-2 items-center text-primary justify-end overflow-hidden">
+        Error checking version <XCircle className="size-4 shrink-0" />
+      </div>
+    );
+  }
+
+  switch (is_latest) {
+    case true:
+      return (
+        <div className="flex gap-2 items-center text-primary justify-end">
+          Latest <CheckCircle2 className="size-4 shrink-0" />
+        </div>
+      );
+    case false:
+      return (
+        <div>
+          <TooltipTrigger delay={0}>
+            <Link
+              className="flex gap-2 items-center text-primary justify-end overflow-hidden"
+              variant="secondary"
+              target="_blank"
+              rel="noopener noreferrer"
+              href="https://docs.codegate.ai/how-to/install#upgrade-codegate"
+            >
+              Update available <CircleAlert className="size-4 shrink-0" />
+            </Link>
+            <Tooltip className="text-right">
+              <span className="block">Current version: {current_version}</span>
+              <span className="block">Latest version: {latest_version}</span>
+            </Tooltip>
+          </TooltipTrigger>
+        </div>
+      );
+    default: {
+      is_latest satisfies never;
+    }
+  }
+};
diff --git a/src/features/dashboard-codegate-status/components/codegate-status.tsx b/src/features/dashboard-codegate-status/components/codegate-status.tsx
new file mode 100644
index 00000000..4464db95
--- /dev/null
+++ b/src/features/dashboard-codegate-status/components/codegate-status.tsx
@@ -0,0 +1,112 @@
+import {
+  Card,
+  CardBody,
+  CardFooter,
+  CardHeader,
+  CardTitle,
+  Cell,
+  Column,
+  Row,
+  Table,
+  TableBody,
+  TableHeader,
+} from "@stacklok/ui-kit";
+
+import { format } from "date-fns";
+import { useState } from "react";
+import { useCodeGateStatus } from "../hooks/use-codegate-status";
+import { CodegateStatusErrorUI } from "./codegate-status-error-ui";
+import {
+  DEFAULT_INTERVAL,
+  PollingInterval,
+  PollIntervalControl,
+} from "./codegate-status-polling-control";
+import { CodegateStatusHealth } from "./codegate-status-health";
+import { CodegateStatusVersion } from "./codegate-status-version";
+import { CodeGateStatusRefreshButton } from "./codegate-status-refresh-button";
+
+export function InnerContent({
+  isError,
+  isPending,
+  data,
+}: Pick<
+  ReturnType<typeof useCodeGateStatus>,
+  "data" | "isPending" | "isError"
+>) {
+  if (!isPending && isError) {
+    return <CodegateStatusErrorUI />;
+  }
+
+  const { health, version } = data || {};
+
+  return (
+    <Table className="h-max" aria-label="CodeGate status checks">
+      <TableHeader className="hidden">
+        <Column isRowHeader>Name</Column>
+        <Column>Value</Column>
+      </TableHeader>
+      <TableBody>
+        <Row className="hover:bg-transparent">
+          <Cell className="pl-0">CodeGate server</Cell>
+          <Cell className="pr-0 text-end">
+            <CodegateStatusHealth isPending={isPending} data={health ?? null} />
+          </Cell>
+        </Row>
+
+        <Row className="hover:bg-transparent">
+          <Cell className="pl-0">CodeGate version</Cell>
+          <Cell className="pr-0 text-end">
+            <CodegateStatusVersion
+              isPending={isPending}
+              data={version ?? null}
+            />
+          </Cell>
+        </Row>
+      </TableBody>
+    </Table>
+  );
+}
+
+export function CodegateStatus() {
+  const [pollingInterval, setPollingInterval] = useState<PollingInterval>(
+    () => DEFAULT_INTERVAL,
+  );
+  const { data, dataUpdatedAt, isPending, isError } =
+    useCodeGateStatus(pollingInterval);
+
+  return (
+    <Card className="h-full flex flex-col">
+      <CardHeader>
+        <CardTitle className="flex justify-between items-center">
+          <span className="block">CodeGate Status</span>
+        </CardTitle>
+
+        <CodeGateStatusRefreshButton
+          className="ml-auto -mr-2"
+          pollingInterval={pollingInterval}
+        />
+      </CardHeader>
+
+      <CardBody className="h-max">
+        <InnerContent data={data} isPending={isPending} isError={isError} />
+      </CardBody>
+
+      <CardFooter className="items-start border-t border-gray-200 mt-auto py-2">
+        <div>
+          <div className="text-sm font-semibold text-secondary">
+            Last checked
+          </div>
+          <div className="text-sm text-secondary">
+            {format(new Date(dataUpdatedAt), "pp")}
+          </div>
+        </div>
+
+        <PollIntervalControl
+          className="ml-auto"
+          pollingInterval={pollingInterval}
+          setPollingInterval={setPollingInterval}
+        />
+      </CardFooter>
+    </Card>
+  );
+}
diff --git a/src/features/dashboard-codegate-status/hooks/use-codegate-status.ts b/src/features/dashboard-codegate-status/hooks/use-codegate-status.ts
new file mode 100644
index 00000000..bf7712de
--- /dev/null
+++ b/src/features/dashboard-codegate-status/hooks/use-codegate-status.ts
@@ -0,0 +1,38 @@
+import { queryOptions, useQuery } from "@tanstack/react-query";
+
+import { getCodeGateHealth } from "../lib/get-codegate-health";
+import { getVersionStatus } from "../lib/get-version-status";
+import {
+  PollingInterval,
+  POLLING_INTERVAl,
+} from "../components/codegate-status-polling-control";
+
+export function getQueryOptionsCodeGateStatus(
+  pollingInterval: PollingInterval,
+) {
+  return queryOptions({
+    queryFn: async () => {
+      const health = await getCodeGateHealth();
+      const version = await getVersionStatus();
+
+      return {
+        health,
+        version,
+      };
+    },
+    queryKey: ["useHealthCheck", { pollingInterval }],
+    refetchInterval: POLLING_INTERVAl[pollingInterval].value,
+    staleTime: Infinity,
+    gcTime: Infinity,
+    refetchIntervalInBackground: true,
+    refetchOnMount: true,
+    refetchOnReconnect: true,
+    refetchOnWindowFocus: true,
+    retry: false,
+  });
+}
+
+export const useCodeGateStatus = (pollingInterval: PollingInterval) =>
+  useQuery({
+    ...getQueryOptionsCodeGateStatus(pollingInterval),
+  });
diff --git a/src/features/dashboard-codegate-status/lib/get-codegate-health.ts b/src/features/dashboard-codegate-status/lib/get-codegate-health.ts
new file mode 100644
index 00000000..a22f521c
--- /dev/null
+++ b/src/features/dashboard-codegate-status/lib/get-codegate-health.ts
@@ -0,0 +1,18 @@
+export enum HealthStatus {
+  HEALTHY = "Healthy",
+  UNHEALTHY = "Unhealthy",
+}
+
+type HealthResponse = { status: "healthy" | unknown } | null;
+
+export const getCodeGateHealth = async (): Promise<HealthStatus | null> => {
+  const resp = await fetch(
+    new URL("/health", import.meta.env.VITE_BASE_API_URL),
+  );
+  const data = (await resp.json()) as unknown as HealthResponse;
+
+  if (data?.status === "healthy") return HealthStatus.HEALTHY;
+  if (data?.status !== "healthy") return HealthStatus.UNHEALTHY;
+
+  return null;
+};
diff --git a/src/features/dashboard-codegate-status/lib/get-version-status.ts b/src/features/dashboard-codegate-status/lib/get-version-status.ts
new file mode 100644
index 00000000..287113f3
--- /dev/null
+++ b/src/features/dashboard-codegate-status/lib/get-version-status.ts
@@ -0,0 +1,15 @@
+export type VersionResponse = {
+  current_version: string;
+  latest_version: string;
+  is_latest: boolean | null;
+  error: string | null;
+} | null;
+
+export const getVersionStatus = async (): Promise<VersionResponse | null> => {
+  const resp = await fetch(
+    new URL("/dashboard/version", import.meta.env.VITE_BASE_API_URL),
+  );
+  const data = (await resp.json()) as unknown as VersionResponse;
+
+  return data;
+};
diff --git a/src/features/dashboard/components/__tests__/card-codegate-status.test.tsx b/src/features/dashboard/components/__tests__/card-codegate-status.test.tsx
deleted file mode 100644
index 838cc424..00000000
--- a/src/features/dashboard/components/__tests__/card-codegate-status.test.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-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
deleted file mode 100644
index 9ed02518..00000000
--- a/src/features/dashboard/components/card-codegate-status.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import {
-  Card,
-  CardBody,
-  CardFooter,
-  CardHeader,
-  CardTitle,
-  Cell,
-  Column,
-  Label,
-  Row,
-  Select,
-  SelectButton,
-  Table,
-  TableBody,
-  TableHeader,
-  TDropdownItemOrSection,
-} from "@stacklok/ui-kit";
-
-import { useQuery } from "@tanstack/react-query";
-import { format } from "date-fns";
-import { CheckCircle2, LoaderCircle, XCircle } 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 INTERVAL_SELECT_ITEMS: TDropdownItemOrSection[] = Object.entries(
-  INTERVAL,
-).map(([key, { name }]) => {
-  return { textValue: name, id: key };
-});
-
-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-secondary 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-primary justify-end">
-          {Status.HEALTHY} <CheckCircle2 className="size-4" />
-        </div>
-      );
-    case Status.UNHEALTHY:
-      return (
-        <div className="flex gap-2 items-center text-primary 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-base font-semibold text-secondary text-center">
-        An error occurred
-      </div>
-      <div className="text-sm text-secondary text-center text-balance">
-        If this issue persists, please reach out to us on{" "}
-        <a
-          className="underline text-secondary"
-          href="https://discord.gg/stacklok"
-          rel="noopener noreferrer"
-          target="_blank"
-        >
-          Discord
-        </a>{" "}
-        or open a new{" "}
-        <a
-          className="underline text-secondary"
-          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 (
-    <Select
-      className={className}
-      onSelectionChange={(v) => setPollingInterval(v.toString() as Interval)}
-      items={INTERVAL_SELECT_ITEMS}
-      defaultSelectedKey={pollingInterval}
-    >
-      <Label className="w-full text-right font-semibold text-secondary pr-2 -mb-1">
-        Check for updates
-      </Label>
-      <SelectButton
-        isBorderless
-        className="h-7 max-w-36 [&>span>span]:text-right [&>span>span]:justify-end !gap-0 text-secondary"
-      />
-    </Select>
-  );
-}
-
-export function InnerContent({
-  isError,
-  isPending,
-  data,
-}: Pick<ReturnType<typeof useStatus>, "data" | "isPending" | "isError">) {
-  if (!isPending && isError) {
-    return <ErrorUI />;
-  }
-
-  return (
-    <Table className="h-max" aria-label="CodeGate status checks">
-      <TableHeader className="hidden">
-        <Column isRowHeader>Name</Column>
-        <Column>Value</Column>
-      </TableHeader>
-      <TableBody>
-        <Row className="hover:bg-transparent">
-          <Cell className="pl-0">CodeGate server</Cell>
-          <Cell className="pr-0 text-end">
-            <StatusText isPending={isPending} status={data ?? null} />
-          </Cell>
-        </Row>
-      </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>
-
-      <CardBody className="h-max">
-        <InnerContent data={data} isPending={isPending} isError={isError} />
-      </CardBody>
-
-      <CardFooter className="items-start border-t border-gray-200 mt-auto py-2">
-        <div>
-          <div className="text-sm font-semibold text-secondary">
-            Last checked
-          </div>
-          <div className="text-sm text-secondary">
-            {format(new Date(dataUpdatedAt), "pp")}
-          </div>
-        </div>
-
-        <PollIntervalControl
-          className="ml-auto"
-          pollingInterval={pollingInterval}
-          setPollingInterval={setPollingInterval}
-        />
-      </CardFooter>
-    </Card>
-  );
-}
diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts
index a2277a5a..46e62cd6 100644
--- a/src/mocks/msw/handlers.ts
+++ b/src/mocks/msw/handlers.ts
@@ -3,7 +3,17 @@ 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("*/health", () =>
+    HttpResponse.json({
+      current_version: "foo",
+      latest_version: "bar",
+      is_latest: false,
+      error: null,
+    }),
+  ),
+  http.get("*/dashboard/version", () =>
+    HttpResponse.json({ status: "healthy" }),
+  ),
   http.get("*/dashboard/messages", () => {
     return HttpResponse.json(mockedPrompts);
   }),
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 7b2c8144..d2d469f2 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -10,6 +10,19 @@ export default {
   theme: {
     ...stacklokTailwindPreset.theme,
     extend: {
+      animation: {
+        "spin-once": "spin-once 0.5s ease-in-out",
+      },
+      keyframes: {
+        "spin-once": {
+          "0%": {
+            transform: "rotate(0deg)",
+          },
+          "100%": {
+            transform: "rotate(180deg)",
+          },
+        },
+      },
       typography: () => ({
         DEFAULT: {
           css: {