diff --git a/ui/litellm-dashboard/src/components/LicenseExpiryBanner.test.tsx b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.test.tsx index 81345dac09f..781654ac2f8 100644 --- a/ui/litellm-dashboard/src/components/LicenseExpiryBanner.test.tsx +++ b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.test.tsx @@ -95,7 +95,7 @@ describe("LicenseExpiryBanner", () => { }); it("should show a warning banner when license expires within 14 days", async () => { - // Now is 2026-03-15T12:00:00Z, exp is 2026-03-22T23:59:59 => ~7.5 days => ceil = 8 + // Now is 2026-03-15T12:00:00Z, exp is 2026-03-22T23:59:59Z (UTC) => ~7.5 days => ceil = 8 mockGetLicenseInfo.mockResolvedValue(makeLicense("2026-03-22")); render(); @@ -106,7 +106,7 @@ describe("LicenseExpiryBanner", () => { }); it("should show singular 'day' when 1 day remains", async () => { - // Now is 2026-03-15T12:00:00Z, exp is 2026-03-15T23:59:59 => ~0.5 days => ceil = 1 + // Now is 2026-03-15T12:00:00Z, exp is 2026-03-15T23:59:59Z (UTC) => ~0.5 days => ceil = 1 mockGetLicenseInfo.mockResolvedValue(makeLicense("2026-03-15")); render(); @@ -173,6 +173,16 @@ describe("LicenseExpiryBanner", () => { }); }); + it("should show a warning when getLicenseInfo API call fails", async () => { + mockGetLicenseInfo.mockRejectedValue(new Error("Network error")); + + render(); + + expect( + await screen.findByText("Unable to verify enterprise license") + ).toBeInTheDocument(); + }); + it("should render nothing when expiration_date is unparseable", async () => { mockGetLicenseInfo.mockResolvedValue(makeLicense("N/A")); diff --git a/ui/litellm-dashboard/src/components/LicenseExpiryBanner.tsx b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.tsx index b327c571991..615be1ff979 100644 --- a/ui/litellm-dashboard/src/components/LicenseExpiryBanner.tsx +++ b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.tsx @@ -6,26 +6,55 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { getLicenseInfo, LicenseInfo } from "./networking"; const EXPIRY_WARNING_DAYS = 14; +/** Re-fetch license info every 6 hours so long-lived sessions pick up expiry. */ +const REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; export const LicenseExpiryBanner: React.FC = () => { const { accessToken } = useAuthorized(); const [licenseInfo, setLicenseInfo] = useState(null); + const [fetchError, setFetchError] = useState(false); useEffect(() => { if (!accessToken) return; let cancelled = false; - getLicenseInfo(accessToken) - .then((info) => { - if (!cancelled) setLicenseInfo(info); - }) - .catch(() => null); + + const fetchLicense = () => { + getLicenseInfo(accessToken) + .then((info) => { + if (!cancelled) { + setLicenseInfo(info); + setFetchError(false); + } + }) + .catch(() => { + if (!cancelled) setFetchError(true); + }); + }; + + fetchLicense(); + const intervalId = setInterval(fetchLicense, REFRESH_INTERVAL_MS); return () => { cancelled = true; + clearInterval(intervalId); }; }, [accessToken]); + if (fetchError) { + return ( + + ); + } + if (!licenseInfo?.has_license || !licenseInfo.expiration_date) { return null; }