From dfbd4131ca00a4bd66210dc0fb649176fdd49545 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Wed, 4 Feb 2026 19:01:12 -0800 Subject: [PATCH 1/4] feat(web): implement version check and update notification system - Added `useVersionCheck` hook to monitor application version and notify users of updates. - Integrated version check into the `SidebarIconRow` component, displaying a download icon when an update is available. - Introduced a `BUILD_VERSION` constant in the webpack configuration to manage versioning. - Created a `version.json` file during production builds to store the current version. - Added tests for the `useVersionCheck` hook to ensure correct functionality and behavior. --- packages/web/declaration.d.ts | 2 + .../src/common/hooks/useVersionCheck.test.ts | 144 ++++++++++++++++++ .../web/src/common/hooks/useVersionCheck.ts | 105 +++++++++++++ .../web/src/components/Icons/Download.tsx | 7 + .../Sidebar/SidebarIconRow/SidebarIconRow.tsx | 17 +++ packages/web/webpack.config.mjs | 26 ++++ 6 files changed, 301 insertions(+) create mode 100644 packages/web/src/common/hooks/useVersionCheck.test.ts create mode 100644 packages/web/src/common/hooks/useVersionCheck.ts create mode 100644 packages/web/src/components/Icons/Download.tsx diff --git a/packages/web/declaration.d.ts b/packages/web/declaration.d.ts index 5b5bf1821..465277980 100644 --- a/packages/web/declaration.d.ts +++ b/packages/web/declaration.d.ts @@ -26,3 +26,5 @@ declare module "*.jpg" { declare module "*.jpeg" { export = imageUrl; } + +declare const BUILD_VERSION: string; diff --git a/packages/web/src/common/hooks/useVersionCheck.test.ts b/packages/web/src/common/hooks/useVersionCheck.test.ts new file mode 100644 index 000000000..1f7325d2c --- /dev/null +++ b/packages/web/src/common/hooks/useVersionCheck.test.ts @@ -0,0 +1,144 @@ +import { act } from "react"; +import { renderHook } from "@testing-library/react"; +import { useVersionCheck } from "@web/common/hooks/useVersionCheck"; + +const MIN_HIDDEN_DURATION_MS = 30_000; +const BACKUP_CHECK_INTERVAL_MS = 5 * 60 * 1000; + +describe("useVersionCheck", () => { + let visibilityState = "visible"; + const flushPromises = async () => { + await Promise.resolve(); + await Promise.resolve(); + }; + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2026-02-05T00:00:00.000Z")); + + visibilityState = "visible"; + Object.defineProperty(document, "visibilityState", { + configurable: true, + get: () => visibilityState, + }); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ version: "dev" }), + }) as typeof fetch; + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + }); + + it("checks when tab becomes visible after being hidden long enough", async () => { + renderHook(() => useVersionCheck()); + await act(async () => { + await flushPromises(); + }); + (global.fetch as jest.Mock).mockClear(); + + act(() => { + visibilityState = "hidden"; + document.dispatchEvent(new Event("visibilitychange")); + jest.advanceTimersByTime(MIN_HIDDEN_DURATION_MS + 1_000); + visibilityState = "visible"; + document.dispatchEvent(new Event("visibilitychange")); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("does not check when tab becomes visible after a short hide", async () => { + renderHook(() => useVersionCheck()); + await act(async () => { + await flushPromises(); + }); + (global.fetch as jest.Mock).mockClear(); + + act(() => { + visibilityState = "hidden"; + document.dispatchEvent(new Event("visibilitychange")); + jest.advanceTimersByTime(MIN_HIDDEN_DURATION_MS - 10_000); + visibilityState = "visible"; + document.dispatchEvent(new Event("visibilitychange")); + }); + + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("cleans up the visibility listener on unmount", () => { + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + const removeEventListenerSpy = jest.spyOn(document, "removeEventListener"); + + const { unmount } = renderHook(() => useVersionCheck()); + + const handler = addEventListenerSpy.mock.calls.find( + ([eventName]) => eventName === "visibilitychange", + )?.[1]; + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith( + "visibilitychange", + handler, + ); + + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); + + it("runs the backup poll on the interval", async () => { + renderHook(() => useVersionCheck()); + await act(async () => { + await flushPromises(); + }); + (global.fetch as jest.Mock).mockClear(); + + act(() => { + jest.advanceTimersByTime(BACKUP_CHECK_INTERVAL_MS); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("prevents concurrent checks", async () => { + let resolveFetch: ((value: unknown) => void) | undefined; + const fetchPromise = new Promise((resolve) => { + resolveFetch = resolve; + }); + + renderHook(() => useVersionCheck()); + await act(async () => { + await flushPromises(); + }); + + global.fetch = jest.fn().mockReturnValue(fetchPromise) as typeof fetch; + (global.fetch as jest.Mock).mockClear(); + + act(() => { + visibilityState = "hidden"; + document.dispatchEvent(new Event("visibilitychange")); + jest.advanceTimersByTime(MIN_HIDDEN_DURATION_MS + 1_000); + visibilityState = "visible"; + document.dispatchEvent(new Event("visibilitychange")); + + visibilityState = "hidden"; + document.dispatchEvent(new Event("visibilitychange")); + jest.advanceTimersByTime(MIN_HIDDEN_DURATION_MS + 1_000); + visibilityState = "visible"; + document.dispatchEvent(new Event("visibilitychange")); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + + act(() => { + resolveFetch?.({ + ok: true, + json: async () => ({ version: "dev" }), + }); + }); + }); +}); diff --git a/packages/web/src/common/hooks/useVersionCheck.ts b/packages/web/src/common/hooks/useVersionCheck.ts new file mode 100644 index 000000000..5a06838d9 --- /dev/null +++ b/packages/web/src/common/hooks/useVersionCheck.ts @@ -0,0 +1,105 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { z } from "zod"; +import { IS_DEV } from "@web/common/constants/env.constants"; + +const MIN_HIDDEN_DURATION_MS = 30_000; +const BACKUP_CHECK_INTERVAL_MS = 5 * 60 * 1000; +const CURRENT_VERSION = + typeof BUILD_VERSION === "string" ? BUILD_VERSION : "dev"; +const versionResponseSchema = z.object({ + version: z.string().optional(), +}); + +export interface VersionCheckResult { + isUpdateAvailable: boolean; + currentVersion: string; +} + +export const useVersionCheck = (): VersionCheckResult => { + const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); + const hiddenAtRef = useRef(null); + const isCheckingRef = useRef(false); + + const checkVersion = useCallback(async () => { + if (isCheckingRef.current) { + return; + } + + isCheckingRef.current = true; + + try { + const response = await fetch(`/version.json?t=${Date.now()}`, { + cache: "no-store", + headers: { "Cache-Control": "no-cache" }, + }); + + if (!response.ok) { + return; + } + + const parsedResponse = versionResponseSchema.safeParse( + await response.json(), + ); + + if (!parsedResponse.success) { + return; + } + + const { version: serverVersion } = parsedResponse.data; + + if (!serverVersion) { + return; + } + + setIsUpdateAvailable(serverVersion !== CURRENT_VERSION); + } catch (error) { + console.debug("Version check failed:", error); + } finally { + isCheckingRef.current = false; + } + }, []); + + useEffect(() => { + if (IS_DEV) { + return; + } + + const handleVisibilityChange = () => { + if (document.visibilityState === "hidden") { + hiddenAtRef.current = Date.now(); + return; + } + + if (document.visibilityState !== "visible") { + return; + } + + const hiddenAt = hiddenAtRef.current; + hiddenAtRef.current = null; + + if (hiddenAt === null) { + return; + } + + const hiddenDuration = Date.now() - hiddenAt; + if (hiddenDuration >= MIN_HIDDEN_DURATION_MS) { + checkVersion(); + } + }; + + checkVersion(); + + document.addEventListener("visibilitychange", handleVisibilityChange); + const backupInterval = window.setInterval( + checkVersion, + BACKUP_CHECK_INTERVAL_MS, + ); + + return () => { + document.removeEventListener("visibilitychange", handleVisibilityChange); + clearInterval(backupInterval); + }; + }, [checkVersion]); + + return { isUpdateAvailable, currentVersion: CURRENT_VERSION }; +}; diff --git a/packages/web/src/components/Icons/Download.tsx b/packages/web/src/components/Icons/Download.tsx new file mode 100644 index 000000000..e1b55d1e5 --- /dev/null +++ b/packages/web/src/components/Icons/Download.tsx @@ -0,0 +1,7 @@ +import styled from "styled-components"; +import { ArrowsClockwiseIcon } from "@phosphor-icons/react"; +import { iconStyles } from "./styled"; + +export const DownloadIcon = styled(ArrowsClockwiseIcon)` + ${iconStyles} +`; diff --git a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx index 0afaccc00..ec336f4f9 100644 --- a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx +++ b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx @@ -1,7 +1,9 @@ +import { useVersionCheck } from "@web/common/hooks/useVersionCheck"; import { theme } from "@web/common/styles/theme"; import { getModifierKeyIcon } from "@web/common/utils/shortcut/shortcut.util"; import { CalendarIcon } from "@web/components/Icons/Calendar"; import { CommandIcon } from "@web/components/Icons/Command"; +import { DownloadIcon } from "@web/components/Icons/Download"; import { SpinnerIcon } from "@web/components/Icons/Spinner"; import { TodoIcon } from "@web/components/Icons/Todo"; import { Text } from "@web/components/Text"; @@ -22,6 +24,13 @@ export const SidebarIconRow = () => { const tab = useAppSelector(selectSidebarTab); const gCalImport = useAppSelector(selectImportGCalState); const isCmdPaletteOpen = useAppSelector(selectIsCmdPaletteOpen); + const { isUpdateAvailable } = useVersionCheck(); + + const handleUpdateReload = () => { + const url = new URL(window.location.href); + url.searchParams.set("update", Date.now().toString()); + window.location.replace(url.toString()); + }; const toggleCmdPalette = () => { if (isCmdPaletteOpen) { @@ -88,6 +97,14 @@ export const SidebarIconRow = () => { ) : undefined} + {isUpdateAvailable ? ( + + + + ) : undefined} ); diff --git a/packages/web/webpack.config.mjs b/packages/web/webpack.config.mjs index 17710a6c0..44d00b542 100644 --- a/packages/web/webpack.config.mjs +++ b/packages/web/webpack.config.mjs @@ -65,6 +65,9 @@ const loadEnvFile = (envName) => { */ export default (env, argv) => { const IS_DEV = argv.mode === "development"; + const BUILD_VERSION = IS_DEV + ? "dev" + : `${Date.now()}-${Math.random().toString(36).substring(7)}`; const ENVIRONMENT = argv.nodeEnv || "local"; loadEnvFile(ENVIRONMENT); @@ -110,6 +113,7 @@ export default (env, argv) => { // Define process.env as an object literal (not a JSON string) // This allows both process.env.KEY and process.env["KEY"] bracket notation to work "process.env": envObject, + BUILD_VERSION: JSON.stringify(BUILD_VERSION), }), new HtmlWebpackPlugin({ filename: "index.html", @@ -129,6 +133,28 @@ export default (env, argv) => { _plugins.push(new BundleAnalyzerPlugin()); } + if (!IS_DEV) { + _plugins.push({ + apply: (compiler) => { + compiler.hooks.emit.tapAsync( + "GenerateVersionPlugin", + (compilation, callback) => { + const versionContent = JSON.stringify( + { version: BUILD_VERSION }, + null, + 2, + ); + compilation.assets["version.json"] = { + source: () => versionContent, + size: () => versionContent.length, + }; + callback(); + }, + ); + }, + }); + } + return { entry: "./src/index.tsx", // got devtool sourcemap errors with: eval, eval-cheap-source-map From 8342361532dcebd2185e593f94e572ea6083fb3b Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Wed, 4 Feb 2026 19:18:55 -0800 Subject: [PATCH 2/4] feat(web): add RefreshIcon component and update SidebarIconRow to use it - Introduced a new `RefreshIcon` component styled with `styled-components` and integrated from `@phosphor-icons/react`. - Replaced the `DownloadIcon` in the `SidebarIconRow` component with the new `RefreshIcon` to enhance the user interface for update notifications. --- .../web/src/components/Icons/{Download.tsx => Refresh.tsx} | 2 +- .../components/Sidebar/SidebarIconRow/SidebarIconRow.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename packages/web/src/components/Icons/{Download.tsx => Refresh.tsx} (73%) diff --git a/packages/web/src/components/Icons/Download.tsx b/packages/web/src/components/Icons/Refresh.tsx similarity index 73% rename from packages/web/src/components/Icons/Download.tsx rename to packages/web/src/components/Icons/Refresh.tsx index e1b55d1e5..e3a39b1fa 100644 --- a/packages/web/src/components/Icons/Download.tsx +++ b/packages/web/src/components/Icons/Refresh.tsx @@ -2,6 +2,6 @@ import styled from "styled-components"; import { ArrowsClockwiseIcon } from "@phosphor-icons/react"; import { iconStyles } from "./styled"; -export const DownloadIcon = styled(ArrowsClockwiseIcon)` +export const RefreshIcon = styled(ArrowsClockwiseIcon)` ${iconStyles} `; diff --git a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx index ec336f4f9..a7461f2c5 100644 --- a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx +++ b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx @@ -3,7 +3,7 @@ import { theme } from "@web/common/styles/theme"; import { getModifierKeyIcon } from "@web/common/utils/shortcut/shortcut.util"; import { CalendarIcon } from "@web/components/Icons/Calendar"; import { CommandIcon } from "@web/components/Icons/Command"; -import { DownloadIcon } from "@web/components/Icons/Download"; +import { RefreshIcon } from "@web/components/Icons/Refresh"; import { SpinnerIcon } from "@web/components/Icons/Spinner"; import { TodoIcon } from "@web/components/Icons/Todo"; import { Text } from "@web/components/Text"; @@ -102,7 +102,7 @@ export const SidebarIconRow = () => { description="Update available" onClick={handleUpdateReload} > - + ) : undefined} From 76b7605771ffc2ba92a25d85f5c1dff5641e2bee Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Wed, 4 Feb 2026 19:26:17 -0800 Subject: [PATCH 3/4] feat(web): enhance version check functionality and update tests - Updated `useVersionCheck` hook to include detailed comments on version checking behavior, including checks on initial mount, tab visibility, and polling intervals. - Added tests for the `useVersionCheck` hook to verify version checks on initial mount and ensure no checks occur in development mode. - Simplified the `handleUpdateReload` function in `SidebarIconRow` to directly reload the window instead of modifying the URL. --- .../src/common/hooks/useVersionCheck.test.ts | 41 +++++++++++++++++++ .../web/src/common/hooks/useVersionCheck.ts | 10 +++++ .../Sidebar/SidebarIconRow/SidebarIconRow.tsx | 4 +- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/web/src/common/hooks/useVersionCheck.test.ts b/packages/web/src/common/hooks/useVersionCheck.test.ts index 1f7325d2c..92871bb02 100644 --- a/packages/web/src/common/hooks/useVersionCheck.test.ts +++ b/packages/web/src/common/hooks/useVersionCheck.test.ts @@ -2,6 +2,13 @@ import { act } from "react"; import { renderHook } from "@testing-library/react"; import { useVersionCheck } from "@web/common/hooks/useVersionCheck"; +let mockIsDev = false; +jest.mock("@web/common/constants/env.constants", () => ({ + get IS_DEV() { + return mockIsDev; + }, +})); + const MIN_HIDDEN_DURATION_MS = 30_000; const BACKUP_CHECK_INTERVAL_MS = 5 * 60 * 1000; @@ -13,6 +20,7 @@ describe("useVersionCheck", () => { }; beforeEach(() => { + mockIsDev = false; jest.useFakeTimers(); jest.setSystemTime(new Date("2026-02-05T00:00:00.000Z")); @@ -33,6 +41,39 @@ describe("useVersionCheck", () => { jest.clearAllMocks(); }); + it("checks version on initial mount", async () => { + renderHook(() => useVersionCheck()); + await act(async () => { + await flushPromises(); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringMatching(/^\/version\.json\?t=\d+$/), + expect.objectContaining({ + cache: "no-store", + headers: { "Cache-Control": "no-cache" }, + }), + ); + }); + + it("does not check for updates in development mode", async () => { + mockIsDev = true; + + renderHook(() => useVersionCheck()); + await act(async () => { + await flushPromises(); + }); + + expect(global.fetch).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(BACKUP_CHECK_INTERVAL_MS); + }); + + expect(global.fetch).not.toHaveBeenCalled(); + }); + it("checks when tab becomes visible after being hidden long enough", async () => { renderHook(() => useVersionCheck()); await act(async () => { diff --git a/packages/web/src/common/hooks/useVersionCheck.ts b/packages/web/src/common/hooks/useVersionCheck.ts index 5a06838d9..30f37854b 100644 --- a/packages/web/src/common/hooks/useVersionCheck.ts +++ b/packages/web/src/common/hooks/useVersionCheck.ts @@ -15,6 +15,16 @@ export interface VersionCheckResult { currentVersion: string; } +/** + * Checks for new application versions by polling `/version.json`. + * + * Performs version checks: + * - On initial mount + * - When the tab becomes visible after being hidden for 30+ seconds + * - Every 5 minutes as a backup poll + * + * Disabled in development mode. + */ export const useVersionCheck = (): VersionCheckResult => { const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); const hiddenAtRef = useRef(null); diff --git a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx index a7461f2c5..8c3e5e923 100644 --- a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx +++ b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx @@ -27,9 +27,7 @@ export const SidebarIconRow = () => { const { isUpdateAvailable } = useVersionCheck(); const handleUpdateReload = () => { - const url = new URL(window.location.href); - url.searchParams.set("update", Date.now().toString()); - window.location.replace(url.toString()); + window.location.reload(); }; const toggleCmdPalette = () => { From cd550c5d31172d58e050aa57fd04f5ed078fd74f Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Wed, 4 Feb 2026 19:42:04 -0800 Subject: [PATCH 4/4] feat(web): enhance versioning and error handling in version check - Updated `webpack.config.mjs` to include the current git hash in the `BUILD_VERSION` for better traceability. - Modified `useVersionCheck` hook to log errors using `console.error` instead of `console.debug`, improving error visibility. - Added additional tests for `useVersionCheck` to cover various scenarios, including network failures and version mismatches. - Cleaned up logging in `clearAllBrowserStorage` function by removing unnecessary console logs. - Updated `SidebarIconRow` to provide clearer update notifications with improved tooltip descriptions. --- .../src/common/hooks/useVersionCheck.test.ts | 83 +++++++++++++++++++ .../web/src/common/hooks/useVersionCheck.ts | 2 +- .../utils/cleanup/browser.cleanup.util.ts | 2 - .../Sidebar/SidebarIconRow/SidebarIconRow.tsx | 4 +- packages/web/webpack.config.mjs | 7 +- 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/packages/web/src/common/hooks/useVersionCheck.test.ts b/packages/web/src/common/hooks/useVersionCheck.test.ts index 92871bb02..19eb9b8c3 100644 --- a/packages/web/src/common/hooks/useVersionCheck.test.ts +++ b/packages/web/src/common/hooks/useVersionCheck.test.ts @@ -59,6 +59,8 @@ describe("useVersionCheck", () => { it("does not check for updates in development mode", async () => { mockIsDev = true; + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + const setIntervalSpy = jest.spyOn(window, "setInterval"); renderHook(() => useVersionCheck()); await act(async () => { @@ -66,12 +68,93 @@ describe("useVersionCheck", () => { }); expect(global.fetch).not.toHaveBeenCalled(); + expect(addEventListenerSpy).not.toHaveBeenCalled(); + expect(setIntervalSpy).not.toHaveBeenCalled(); act(() => { jest.advanceTimersByTime(BACKUP_CHECK_INTERVAL_MS); }); expect(global.fetch).not.toHaveBeenCalled(); + + addEventListenerSpy.mockRestore(); + setIntervalSpy.mockRestore(); + }); + + it("sets isUpdateAvailable when server version differs", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ version: "1.0.0" }), + }) as typeof fetch; + + const { result } = renderHook(() => useVersionCheck()); + + await act(async () => { + await flushPromises(); + }); + + expect(result.current.isUpdateAvailable).toBe(true); + }); + + it("keeps isUpdateAvailable false when versions match", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ version: "dev" }), + }) as typeof fetch; + + const { result } = renderHook(() => useVersionCheck()); + + await act(async () => { + await flushPromises(); + }); + + expect(result.current.isUpdateAvailable).toBe(false); + }); + + it("handles network failures without crashing", async () => { + const consoleErrorSpy = jest + .spyOn(console, "error") + .mockImplementation(() => undefined); + + global.fetch = jest + .fn() + .mockRejectedValue(new Error("Network down")) as typeof fetch; + + const { result } = renderHook(() => useVersionCheck()); + + await act(async () => { + await flushPromises(); + }); + + expect(result.current.isUpdateAvailable).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Version check failed:", + expect.any(Error), + ); + + consoleErrorSpy.mockRestore(); + }); + + it("ignores invalid version payloads", async () => { + const consoleErrorSpy = jest + .spyOn(console, "error") + .mockImplementation(() => undefined); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ version: 123 }), + }) as typeof fetch; + + const { result } = renderHook(() => useVersionCheck()); + + await act(async () => { + await flushPromises(); + }); + + expect(result.current.isUpdateAvailable).toBe(false); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); }); it("checks when tab becomes visible after being hidden long enough", async () => { diff --git a/packages/web/src/common/hooks/useVersionCheck.ts b/packages/web/src/common/hooks/useVersionCheck.ts index 30f37854b..6737366e7 100644 --- a/packages/web/src/common/hooks/useVersionCheck.ts +++ b/packages/web/src/common/hooks/useVersionCheck.ts @@ -63,7 +63,7 @@ export const useVersionCheck = (): VersionCheckResult => { setIsUpdateAvailable(serverVersion !== CURRENT_VERSION); } catch (error) { - console.debug("Version check failed:", error); + console.error("Version check failed:", error); } finally { isCheckingRef.current = false; } diff --git a/packages/web/src/common/utils/cleanup/browser.cleanup.util.ts b/packages/web/src/common/utils/cleanup/browser.cleanup.util.ts index 2d5eb449b..ee977b7f2 100644 --- a/packages/web/src/common/utils/cleanup/browser.cleanup.util.ts +++ b/packages/web/src/common/utils/cleanup/browser.cleanup.util.ts @@ -47,8 +47,6 @@ export async function clearAllBrowserStorage(): Promise { }); } } - - console.log("Browser storage cleared successfully"); } catch (error) { console.error("Error clearing browser storage:", error); throw error; diff --git a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx index 8c3e5e923..cf1f681df 100644 --- a/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx +++ b/packages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx @@ -92,12 +92,12 @@ export const SidebarIconRow = () => { {gCalImport.importing ? ( - + ) : undefined} {isUpdateAvailable ? ( diff --git a/packages/web/webpack.config.mjs b/packages/web/webpack.config.mjs index 44d00b542..0a8eaaf67 100644 --- a/packages/web/webpack.config.mjs +++ b/packages/web/webpack.config.mjs @@ -1,3 +1,4 @@ +import { execSync } from "child_process"; import dotenv from "dotenv"; import fs from "fs"; import HtmlWebpackPlugin from "html-webpack-plugin"; @@ -65,9 +66,9 @@ const loadEnvFile = (envName) => { */ export default (env, argv) => { const IS_DEV = argv.mode === "development"; - const BUILD_VERSION = IS_DEV - ? "dev" - : `${Date.now()}-${Math.random().toString(36).substring(7)}`; + // git hash makes the build traceable + const GIT_HASH = execSync("git rev-parse --short HEAD").toString().trim(); + const BUILD_VERSION = IS_DEV ? "dev" : `${Date.now()}-${GIT_HASH}`; const ENVIRONMENT = argv.nodeEnv || "local"; loadEnvFile(ENVIRONMENT);