From a8b76be98306e2cb4fe0f7e99542478fccbfd7ba Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Fri, 3 Jul 2026 11:23:25 +0200 Subject: [PATCH 1/8] fix: error state - prevent the next and previous buttons in the `DayChartTitle` component from shrinking - removed the folder and user icons of the `AppSidebar` error component - the error boundary is not reset when we have an error related to: either the user is offline or the api is unreachable --- apps/dashboard/src/app/app.tsx | 6 +++- .../src/components/common/day-chart-title.tsx | 4 +-- .../src/components/errors/error-boundary.tsx | 28 +++++++++++++++---- .../layout/app-sidebar/app-sidebar-error.tsx | 4 +-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/apps/dashboard/src/app/app.tsx b/apps/dashboard/src/app/app.tsx index ef257069..153b9589 100644 --- a/apps/dashboard/src/app/app.tsx +++ b/apps/dashboard/src/app/app.tsx @@ -23,6 +23,7 @@ function makeQueryClient() { queries: { staleTime: 60 * 1000, refetchOnWindowFocus: true, + refetchOnReconnect: true, refetchInterval: 60 * 1000, retry: (failureCount, error) => { if (isTRPCClientError(error)) { @@ -42,7 +43,10 @@ function getQueryClient() { if (typeof window === "undefined") { return makeQueryClient(); } else { - if (!browserQueryClient) browserQueryClient = makeQueryClient(); + if (!browserQueryClient) { + browserQueryClient = makeQueryClient(); + } + return browserQueryClient; } } diff --git a/apps/dashboard/src/components/common/day-chart-title.tsx b/apps/dashboard/src/components/common/day-chart-title.tsx index 77b91b15..f1c125a2 100644 --- a/apps/dashboard/src/components/common/day-chart-title.tsx +++ b/apps/dashboard/src/components/common/day-chart-title.tsx @@ -27,7 +27,7 @@ export const DayChartTitle = ({

@@ -50,7 +50,7 @@ export const DayChartTitle = ({
{ if ( - !isAfter(customRange.start, customRange.end) || - period !== "Custom Range" + (!isAfter(customRange.start, customRange.end) || + period !== "Custom Range") && + !isFetchFailure ) { resetErrorBoundary(); } - }, [customRange.start, customRange.end, period]); + }, [ + customRange.start, + customRange.end, + period, + isFetchFailure, + resetErrorBoundary, + ]); return rest.hasCustomChildren ? ( - rest.customChildren(error.message) + rest.customChildren(errorMessage) ) : ( ); }; diff --git a/apps/dashboard/src/components/layout/app-sidebar/app-sidebar-error.tsx b/apps/dashboard/src/components/layout/app-sidebar/app-sidebar-error.tsx index bc0f3962..b3978780 100644 --- a/apps/dashboard/src/components/layout/app-sidebar/app-sidebar-error.tsx +++ b/apps/dashboard/src/components/layout/app-sidebar/app-sidebar-error.tsx @@ -1,5 +1,5 @@ import { useLocation } from "react-router"; -import { Folder, LayoutDashboard, User } from "lucide-react"; +import { LayoutDashboard } from "lucide-react"; import { LinkWithQuery } from "@/components/common/link-with-query"; import { @@ -60,7 +60,6 @@ export const AppSidebarError = ({ errorMessage }: { errorMessage: string }) => { - {errorMessage} @@ -73,7 +72,6 @@ export const AppSidebarError = ({ errorMessage }: { errorMessage: string }) => { - {errorMessage} From 10d444fdf864e103ef50faf84f54e4a31577a8bb Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Sat, 4 Jul 2026 23:53:00 +0200 Subject: [PATCH 2/8] fix: connection lost - added a check to verify if the connection is still present using tanstack query online manager - the error boundary is reset if the connection comes back and we were offline --- apps/dashboard/src/app/app.tsx | 2 ++ .../src/components/errors/error-boundary.tsx | 14 ++++++++ .../src/hooks/use-setup-online-manager.tsx | 35 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 apps/dashboard/src/hooks/use-setup-online-manager.tsx diff --git a/apps/dashboard/src/app/app.tsx b/apps/dashboard/src/app/app.tsx index 153b9589..8c04bac4 100644 --- a/apps/dashboard/src/app/app.tsx +++ b/apps/dashboard/src/app/app.tsx @@ -6,6 +6,7 @@ import superjson from "superjson"; import { FallBackRender } from "@/components/errors/error-boundary"; import { NavigationResetWrapper } from "@/components/layout/navigation-reset-wrapper"; import { useExtensionWebsocket } from "@/hooks/use-extension-websocket"; +import { useSetupOnlineManager } from "@/hooks/use-setup-online-manager"; import { ThemeProvider } from "@/providers/theme-provider"; import { isTRPCClientError, TRPCProvider } from "@/utils/trpc"; import type { AppRouter } from "@repo/trpc/router"; @@ -72,6 +73,7 @@ export const App = () => { ); useExtensionWebsocket(); + useSetupOnlineManager(); return ( diff --git a/apps/dashboard/src/components/errors/error-boundary.tsx b/apps/dashboard/src/components/errors/error-boundary.tsx index d67704ca..a715eb51 100644 --- a/apps/dashboard/src/components/errors/error-boundary.tsx +++ b/apps/dashboard/src/components/errors/error-boundary.tsx @@ -4,6 +4,7 @@ import { TriangleAlert } from "lucide-react"; import { usePeriodStore } from "@/stores/period/period-store"; import { AppRouter } from "@repo/trpc/router"; +import { onlineManager } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; type BaseErrorProps = { @@ -54,6 +55,7 @@ export const FallBackRender = ({ // ! reset the error boundary only: // ! if the range is reset properly (start date before end date or period is not a custom range period) + // ! or if the server was unreachable and is now available // ! don't reset the error boundary if the server is unreachable const isFetchFailure = error.message === "Failed to fetch" || @@ -78,6 +80,18 @@ export const FallBackRender = ({ resetErrorBoundary, ]); + useEffect(() => { + if (!isFetchFailure) { + return; + } + + return onlineManager.subscribe((isOnline) => { + if (isOnline) { + resetErrorBoundary(); + } + }); + }, [isFetchFailure, resetErrorBoundary]); + return rest.hasCustomChildren ? ( rest.customChildren(errorMessage) ) : ( diff --git a/apps/dashboard/src/hooks/use-setup-online-manager.tsx b/apps/dashboard/src/hooks/use-setup-online-manager.tsx new file mode 100644 index 00000000..5bfe2e40 --- /dev/null +++ b/apps/dashboard/src/hooks/use-setup-online-manager.tsx @@ -0,0 +1,35 @@ +import { useEffect } from "react"; + +import { trpcLoaderClient } from "@/utils/trpc"; +import { onlineManager } from "@tanstack/react-query"; + +export const useSetupOnlineManager = () => { + useEffect(() => { + onlineManager.setEventListener((setOnline) => { + const check = async () => { + try { + await trpcLoaderClient.auth.checkAuthStatus.query(); + setOnline(true); + } catch { + setOnline(false); + } + }; + + const setOffline = () => setOnline(false); + + const interval = setInterval(check, 60 * 1000); + window.addEventListener("online", check); + window.addEventListener("offline", setOffline); + window.addEventListener("focus", check); + + check(); + + return () => { + clearInterval(interval); + window.removeEventListener("online", check); + window.removeEventListener("offline", setOffline); + window.removeEventListener("focus", check); + }; + }); + }, []); +}; From 7ab960d9f601ac7a04a25ac4785266fd11b18d24 Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Sun, 5 Jul 2026 01:11:48 +0200 Subject: [PATCH 3/8] fix: routes error - fixed the error of the dashboard crashing when navigating to a project and the server is unreachable using react router ErrorBoundary - added an `AppErrorBoundary` component to play the role of the error boundary of the root app component - extracted the logic of creation of the tanstack query client in a separate file called `query-client` --- apps/dashboard/src/app/app-error-boundary.tsx | 91 +++++++++++++++++++ apps/dashboard/src/app/app.tsx | 59 ++---------- apps/dashboard/src/app/router.tsx | 2 + apps/dashboard/src/utils/query-client.ts | 37 ++++++++ 4 files changed, 137 insertions(+), 52 deletions(-) create mode 100644 apps/dashboard/src/app/app-error-boundary.tsx create mode 100644 apps/dashboard/src/utils/query-client.ts diff --git a/apps/dashboard/src/app/app-error-boundary.tsx b/apps/dashboard/src/app/app-error-boundary.tsx new file mode 100644 index 00000000..5b00f292 --- /dev/null +++ b/apps/dashboard/src/app/app-error-boundary.tsx @@ -0,0 +1,91 @@ +import { useState } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { Link } from "react-router"; +import superjson from "superjson"; + +import { FallBackRender } from "@/components/errors/error-boundary"; +import { SuspenseBoundary } from "@/components/errors/suspense-boundary"; +import { AppSidebar } from "@/components/layout/app-sidebar/app-sidebar"; +import { AppSidebarError } from "@/components/layout/app-sidebar/app-sidebar-error"; +import { AppSidebarSkeleton } from "@/components/layout/app-sidebar/app-sidebar-skeleton"; +import { Footer } from "@/components/layout/footer"; +import { Header } from "@/components/layout/header/header"; +import { NavigationResetWrapper } from "@/components/layout/navigation-reset-wrapper"; +import { ThemeProvider } from "@/providers/theme-provider"; +import { getQueryClient } from "@/utils/query-client"; +import { TRPCProvider } from "@/utils/trpc"; +import { type AppRouter } from "@repo/trpc/router"; +import { ScrollToTopButton } from "@repo/ui/components/scroll-to-top-button"; +import { Button } from "@repo/ui/components/ui/button"; +import { SidebarProvider } from "@repo/ui/components/ui/sidebar"; +import { TooltipProvider } from "@repo/ui/components/ui/tooltip"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; + +export const AppErrorBoundary = () => { + const queryClient = getQueryClient(); + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + httpBatchLink({ + url: import.meta.env.VITE_API_URL, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + transformer: superjson, + }), + ], + }), + ); + + return ( + + + + + + + + ( + ( + + )} + /> + )} + > + } + > + + + + +
+
+
+

An error occurred

+ +
+
+
+ +
+
+
+
+
+
+ ); +}; diff --git a/apps/dashboard/src/app/app.tsx b/apps/dashboard/src/app/app.tsx index 8c04bac4..43af6b99 100644 --- a/apps/dashboard/src/app/app.tsx +++ b/apps/dashboard/src/app/app.tsx @@ -1,57 +1,22 @@ import { useState } from "react"; -import { ErrorBoundary } from "react-error-boundary"; import { Outlet } from "react-router"; import superjson from "superjson"; -import { FallBackRender } from "@/components/errors/error-boundary"; import { NavigationResetWrapper } from "@/components/layout/navigation-reset-wrapper"; import { useExtensionWebsocket } from "@/hooks/use-extension-websocket"; import { useSetupOnlineManager } from "@/hooks/use-setup-online-manager"; import { ThemeProvider } from "@/providers/theme-provider"; -import { isTRPCClientError, TRPCProvider } from "@/utils/trpc"; +import { getQueryClient } from "@/utils/query-client"; +import { TRPCProvider } from "@/utils/trpc"; import type { AppRouter } from "@repo/trpc/router"; import { ScrollToTopButton } from "@repo/ui/components/scroll-to-top-button"; import { SidebarProvider } from "@repo/ui/components/ui/sidebar"; import { Toaster } from "@repo/ui/components/ui/sonner"; import { TooltipProvider } from "@repo/ui/components/ui/tooltip"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; -function makeQueryClient() { - return new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, - refetchOnWindowFocus: true, - refetchOnReconnect: true, - refetchInterval: 60 * 1000, - retry: (failureCount, error) => { - if (isTRPCClientError(error)) { - return failureCount < 1; - } - - return failureCount < 3; - }, - }, - }, - }); -} - -let browserQueryClient: QueryClient | undefined = undefined; - -function getQueryClient() { - if (typeof window === "undefined") { - return makeQueryClient(); - } else { - if (!browserQueryClient) { - browserQueryClient = makeQueryClient(); - } - - return browserQueryClient; - } -} - export const App = () => { const queryClient = getQueryClient(); const [trpcClient] = useState(() => @@ -82,20 +47,10 @@ export const App = () => { - ( - - )} - > - - - - - + + + + diff --git a/apps/dashboard/src/app/router.tsx b/apps/dashboard/src/app/router.tsx index 88382bc4..f2889128 100644 --- a/apps/dashboard/src/app/router.tsx +++ b/apps/dashboard/src/app/router.tsx @@ -29,12 +29,14 @@ import { redirectToNotFoundLoader } from "@/loaders/redirect-to-not-found-loader import { rootLoader } from "@/loaders/root-loader"; import { App } from "./app"; +import { AppErrorBoundary } from "./app-error-boundary"; import { RegisterFinish } from "./pages/auth/register-finish"; import { Profile } from "./pages/profile/profile"; const router = createBrowserRouter([ { element: , + ErrorBoundary: () => , children: [ { element: , diff --git a/apps/dashboard/src/utils/query-client.ts b/apps/dashboard/src/utils/query-client.ts new file mode 100644 index 00000000..631f5b9a --- /dev/null +++ b/apps/dashboard/src/utils/query-client.ts @@ -0,0 +1,37 @@ +import { QueryClient } from "@tanstack/react-query"; + +import { isTRPCClientError } from "./trpc"; + +const makeQueryClient = () => { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchInterval: 60 * 1000, + retry: (failureCount, error) => { + if (isTRPCClientError(error)) { + return failureCount < 1; + } + + return failureCount < 3; + }, + }, + }, + }); +}; + +let browserQueryClient: QueryClient | undefined = undefined; + +export const getQueryClient = () => { + if (typeof window === "undefined") { + return makeQueryClient(); + } else { + if (!browserQueryClient) { + browserQueryClient = makeQueryClient(); + } + + return browserQueryClient; + } +}; From bde282a4540024af3ceb3f8383f368d69213e244 Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Mon, 6 Jul 2026 13:01:42 +0200 Subject: [PATCH 4/8] fix: online manager and svg container filling - removed the focus event listener of the online manager to avoid making too many requests - added a health query in the trpc procedures and used it for the health check in the online manager. This works for authenticated and non-authenticated pages. The old method consisting in using the checkAuthStatus procedure would fail on non-authenticated pages like /login, giving false positives (server unreachable even though it was reachable) and pausing mutations - fixed the issue of the animated night svg image on the auth pages not taking the whole page height when the page height was over the screen height --- apps/api/src/app-router/providers/app-router.provider.ts | 3 +++ apps/dashboard/src/app/pages/auth/forgot-password.tsx | 8 +++++--- apps/dashboard/src/app/pages/auth/login.tsx | 8 +++++--- .../app/pages/auth/password-reset-code-verification.tsx | 8 +++++--- apps/dashboard/src/app/pages/auth/register-finish.tsx | 8 +++++--- apps/dashboard/src/app/pages/auth/register.tsx | 8 +++++--- .../src/app/pages/auth/registration-code-verification.tsx | 8 +++++--- apps/dashboard/src/app/pages/auth/reset-password.tsx | 8 +++++--- .../dashboard/src/app/pages/update-email/update-email.tsx | 8 +++++--- apps/dashboard/src/hooks/use-setup-online-manager.tsx | 4 +--- 10 files changed, 44 insertions(+), 27 deletions(-) diff --git a/apps/api/src/app-router/providers/app-router.provider.ts b/apps/api/src/app-router/providers/app-router.provider.ts index c188666c..ff37e9d8 100644 --- a/apps/api/src/app-router/providers/app-router.provider.ts +++ b/apps/api/src/app-router/providers/app-router.provider.ts @@ -16,6 +16,9 @@ export const appRouterProvider = { ...authRouter.procedures(), ...extensionRouter.procedures(), ...analyticsRouter.procedures(), + health: { + ping: trpcService.publicProcedure().query(() => ({ status: "OK" })), + }, }); return appRouter; diff --git a/apps/dashboard/src/app/pages/auth/forgot-password.tsx b/apps/dashboard/src/app/pages/auth/forgot-password.tsx index 502ac971..1f065776 100644 --- a/apps/dashboard/src/app/pages/auth/forgot-password.tsx +++ b/apps/dashboard/src/app/pages/auth/forgot-password.tsx @@ -6,10 +6,12 @@ export const ForgotPassword = () => { usePageTitle("Forgot Password | MoonCode"); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/app/pages/auth/login.tsx b/apps/dashboard/src/app/pages/auth/login.tsx index e46e58b7..618dbc06 100644 --- a/apps/dashboard/src/app/pages/auth/login.tsx +++ b/apps/dashboard/src/app/pages/auth/login.tsx @@ -13,10 +13,12 @@ export const Login = () => { }, []); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/app/pages/auth/password-reset-code-verification.tsx b/apps/dashboard/src/app/pages/auth/password-reset-code-verification.tsx index 829c93f6..a58928f0 100644 --- a/apps/dashboard/src/app/pages/auth/password-reset-code-verification.tsx +++ b/apps/dashboard/src/app/pages/auth/password-reset-code-verification.tsx @@ -6,10 +6,12 @@ export const CodeVerification = () => { usePageTitle("Verify Reset Code | MoonCode"); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/app/pages/auth/register-finish.tsx b/apps/dashboard/src/app/pages/auth/register-finish.tsx index 43d6cfe6..9abf906a 100644 --- a/apps/dashboard/src/app/pages/auth/register-finish.tsx +++ b/apps/dashboard/src/app/pages/auth/register-finish.tsx @@ -6,10 +6,12 @@ export const RegisterFinish = () => { usePageTitle("Register | MoonCode"); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/app/pages/auth/register.tsx b/apps/dashboard/src/app/pages/auth/register.tsx index 58ad5d64..6d50f4f5 100644 --- a/apps/dashboard/src/app/pages/auth/register.tsx +++ b/apps/dashboard/src/app/pages/auth/register.tsx @@ -6,10 +6,12 @@ export const Register = () => { usePageTitle("Register | MoonCode"); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/app/pages/auth/registration-code-verification.tsx b/apps/dashboard/src/app/pages/auth/registration-code-verification.tsx index b08e22df..e114d167 100644 --- a/apps/dashboard/src/app/pages/auth/registration-code-verification.tsx +++ b/apps/dashboard/src/app/pages/auth/registration-code-verification.tsx @@ -6,10 +6,12 @@ export const CodeVerification = () => { usePageTitle("Register | MoonCode"); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/app/pages/auth/reset-password.tsx b/apps/dashboard/src/app/pages/auth/reset-password.tsx index 9373115e..dfdcbb0e 100644 --- a/apps/dashboard/src/app/pages/auth/reset-password.tsx +++ b/apps/dashboard/src/app/pages/auth/reset-password.tsx @@ -6,10 +6,12 @@ export const ResetPassword = () => { usePageTitle("Reset your password | MoonCode"); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/app/pages/update-email/update-email.tsx b/apps/dashboard/src/app/pages/update-email/update-email.tsx index 2b127145..d5f9f7c3 100644 --- a/apps/dashboard/src/app/pages/update-email/update-email.tsx +++ b/apps/dashboard/src/app/pages/update-email/update-email.tsx @@ -6,10 +6,12 @@ export const UpdateEmail = () => { usePageTitle("Update email | MoonCode"); return ( -
- +
+
+ +
-
+
diff --git a/apps/dashboard/src/hooks/use-setup-online-manager.tsx b/apps/dashboard/src/hooks/use-setup-online-manager.tsx index 5bfe2e40..531b2280 100644 --- a/apps/dashboard/src/hooks/use-setup-online-manager.tsx +++ b/apps/dashboard/src/hooks/use-setup-online-manager.tsx @@ -8,7 +8,7 @@ export const useSetupOnlineManager = () => { onlineManager.setEventListener((setOnline) => { const check = async () => { try { - await trpcLoaderClient.auth.checkAuthStatus.query(); + await trpcLoaderClient.health.ping.query(); setOnline(true); } catch { setOnline(false); @@ -20,7 +20,6 @@ export const useSetupOnlineManager = () => { const interval = setInterval(check, 60 * 1000); window.addEventListener("online", check); window.addEventListener("offline", setOffline); - window.addEventListener("focus", check); check(); @@ -28,7 +27,6 @@ export const useSetupOnlineManager = () => { clearInterval(interval); window.removeEventListener("online", check); window.removeEventListener("offline", setOffline); - window.removeEventListener("focus", check); }; }); }, []); From bcf5878867a1e8d0e5edfafe4f3da17a3ab5e967 Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Mon, 6 Jul 2026 23:07:20 +0200 Subject: [PATCH 5/8] chore: auth loader - removed the usage of the native fetch function in the auth-loader file and replaced it with the trpc client loader with the proper try catch blocks with better readability - replaced the manual url search params concatenation with `URLSearchParams` --- apps/dashboard/src/loaders/auth-loader.ts | 104 ++++++++-------------- 1 file changed, 35 insertions(+), 69 deletions(-) diff --git a/apps/dashboard/src/loaders/auth-loader.ts b/apps/dashboard/src/loaders/auth-loader.ts index a47885ee..07af4174 100644 --- a/apps/dashboard/src/loaders/auth-loader.ts +++ b/apps/dashboard/src/loaders/auth-loader.ts @@ -2,30 +2,17 @@ import { redirect } from "react-router"; import { z } from "zod"; import { getCallbackUrl } from "@/utils/get-callback-url"; +import { isTRPCClientError, trpcLoaderClient } from "@/utils/trpc"; import { formatZodError } from "@repo/common/format-zod-error"; import { VSCodeCallbackUrlSchema } from "@repo/common/types-schemas"; -const API_URL = import.meta.env.VITE_API_URL; - // protects routes export const protectedRouteLoader = async () => { try { - const response = await fetch(`${API_URL}/auth.checkAuthStatus`, { - credentials: "include", - }); - - if (!response.ok) { - if (response.status >= 500 || response.status === 0) { - throw new Error("Service temporarily unavailable"); - } else if (response.status === 401) { - throw redirect("/login"); - } else { - throw new Error("Authentication check failed"); - } - } + await trpcLoaderClient.auth.checkAuthStatus.query(); } catch (error) { - if (error instanceof Response && error.headers.get("Location")) { - throw error; + if (isTRPCClientError(error) && error.data?.code === "UNAUTHORIZED") { + throw redirect("/login"); } } }; @@ -36,40 +23,24 @@ export const authRouteLoader = async () => { const clientParam = decodeURIComponent(urlParams.get("client") ?? ""); const callbackUrl = getCallbackUrl(); + // in the case of a vscode login attempt, logout the user from the dashboard if (callbackUrl && clientParam === "vscode") { const validatedCallBackUrl = VSCodeCallbackUrlSchema.safeParse(callbackUrl); - if (!validatedCallBackUrl.success) { throw redirect("/dashboard"); } - // in the case of a vscode login attempt, logout the user from the dashboard - const LOGOUT_URL = import.meta.env.VITE_LOGOUT_URL; try { - const res = await fetch(LOGOUT_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - }); - - await res.json(); + await trpcLoaderClient.auth.logOut.mutate(); } catch { + } finally { return null; } - - return null; } try { - const response = await fetch(`${API_URL}/auth.checkAuthStatus`, { - credentials: "include", - }); - if (response.ok) { - return redirect("/dashboard"); - } - return null; + await trpcLoaderClient.auth.checkAuthStatus.query(); + return redirect("/dashboard"); } catch { return null; } @@ -81,41 +52,29 @@ export const googleAuthLoader = async () => { const callbackUrl = getCallbackUrl(); try { - const response = await fetch(`${API_URL}/auth.checkAuthStatus`, { - credentials: "include", - }); - - if (response.ok) { - return redirect("/dashboard"); - } - - const AUTH_GOOGLE_URL = import.meta.env.VITE_AUTH_GOOGLE_URL; + await trpcLoaderClient.auth.checkAuthStatus.query(); + return redirect("/dashboard"); + } catch {} - const origin = window.location.origin; - let authUrl = `${AUTH_GOOGLE_URL}?state=${encodeURIComponent(origin)}`; + const AUTH_GOOGLE_URL = import.meta.env.VITE_AUTH_GOOGLE_URL; - if (callbackUrl && clientParam === "vscode") { - // the login request comes from the extension - const validatedCallBackUrl = - VSCodeCallbackUrlSchema.safeParse(callbackUrl); + const origin = window.location.origin; + const authUrl = new URL(AUTH_GOOGLE_URL); + authUrl.searchParams.set("state", origin); - if (!validatedCallBackUrl.success) { - throw new Error( - validatedCallBackUrl.error.issues.reduce( - (acc, curr) => acc + curr.message, - "", - ), - ); - } + // the login request comes from the extension + if (callbackUrl && clientParam === "vscode") { + const validatedCallBackUrl = VSCodeCallbackUrlSchema.safeParse(callbackUrl); - authUrl = `${AUTH_GOOGLE_URL}?state=${encodeURIComponent(origin)}&callback=${encodeURIComponent(validatedCallBackUrl.data)}`; + if (!validatedCallBackUrl.success) { + const error = formatZodError(validatedCallBackUrl.error); + throw new Error(error); } - window.location.href = authUrl; - } catch (error) { - console.error(error); - return null; + authUrl.searchParams.set("callback", validatedCallBackUrl.data); } + + window.location.href = authUrl.toString(); }; export const linkGoogleAccountLoader = async () => { @@ -125,8 +84,10 @@ export const linkGoogleAccountLoader = async () => { .VITE_LINKING_GOOGLE_ACCOUNT_URL; const origin = window.location.origin; - const linkingUrl = `${LINKING_GOOGLE_ACCOUNT_URL}?state=${encodeURIComponent(origin)}`; - window.location.href = linkingUrl; + const linkingUrl = new URL(LINKING_GOOGLE_ACCOUNT_URL); + linkingUrl.searchParams.set("state", origin); + + window.location.href = linkingUrl.toString(); }; export const redirectToVSCodeAfterGoogleAuthLoader = async () => { @@ -154,7 +115,12 @@ export const redirectToVSCodeAfterGoogleAuthLoader = async () => { if (!parsedGoogleAuthParams.success) { throw redirect("/dashboard"); } - window.location.href = `${validatedCallBackUrl.data}&token=${encodeURIComponent(parsedGoogleAuthParams.data.token)}&email=${encodeURIComponent(parsedGoogleAuthParams.data.email)}`; + + const redirectUrl = new URL(validatedCallBackUrl.data); + redirectUrl.searchParams.set("token", parsedGoogleAuthParams.data.token); + redirectUrl.searchParams.set("email", parsedGoogleAuthParams.data.email); + + window.location.href = redirectUrl.toString(); } return null; From 92657bb3eaf190b212d8e4d19573ffacf62a2f4a Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Tue, 7 Jul 2026 01:12:08 +0200 Subject: [PATCH 6/8] perf: extension sync with server - added a check to only sync files and languages with the api when they have changed - added two new utility functions `hasLanguagesDataChanged` and `hasFilesDataChanged` to verify if there were changes between the files or languages data since the last sync with the server --- .../src/utils/files/has-files-data-changed.ts | 8 ++ .../languages/has-languages-data-changed.ts | 8 ++ .../src/utils/periodic-sync-data.ts | 78 ++++++++++++------- .../src/utils/time/is-new-day-handler.ts | 2 +- 4 files changed, 65 insertions(+), 31 deletions(-) create mode 100644 apps/vscode-extension/src/utils/files/has-files-data-changed.ts create mode 100644 apps/vscode-extension/src/utils/languages/has-languages-data-changed.ts diff --git a/apps/vscode-extension/src/utils/files/has-files-data-changed.ts b/apps/vscode-extension/src/utils/files/has-files-data-changed.ts new file mode 100644 index 00000000..0c6a7ebb --- /dev/null +++ b/apps/vscode-extension/src/utils/files/has-files-data-changed.ts @@ -0,0 +1,8 @@ +import { isDeepStrictEqual } from "node:util"; + +import { FileDataSync } from "@/types-schemas"; + +export const hasFilesDataChanged = ( + oldFilesData: FileDataSync, + newFilesData: FileDataSync, +) => !isDeepStrictEqual(oldFilesData, newFilesData); diff --git a/apps/vscode-extension/src/utils/languages/has-languages-data-changed.ts b/apps/vscode-extension/src/utils/languages/has-languages-data-changed.ts new file mode 100644 index 00000000..c5d1f245 --- /dev/null +++ b/apps/vscode-extension/src/utils/languages/has-languages-data-changed.ts @@ -0,0 +1,8 @@ +import { isDeepStrictEqual } from "node:util"; + +export const hasLanguagesDataChanged = ( + oldTimeSpentPerLanguage: { + [languageSlug: string]: number; + }, + newTimeSpentPerLanguage: { [languageSlug: string]: number }, +) => !isDeepStrictEqual(oldTimeSpentPerLanguage, newTimeSpentPerLanguage); diff --git a/apps/vscode-extension/src/utils/periodic-sync-data.ts b/apps/vscode-extension/src/utils/periodic-sync-data.ts index 0ea7e9c0..fa4dce27 100644 --- a/apps/vscode-extension/src/utils/periodic-sync-data.ts +++ b/apps/vscode-extension/src/utils/periodic-sync-data.ts @@ -5,9 +5,11 @@ import { getLocaleDate } from "@repo/common/get-locale-date"; import { getLoginContext } from "./auth/login-context"; import { handleInvalidTokenError } from "./errors/handle-invalid-token-error"; +import { hasFilesDataChanged } from "./files/has-files-data-changed"; import { updateFilesDataAfterSync } from "./files/update-files-data-after-sync"; import { getGlobalStateData } from "./global-state/get-global-state-data"; import { updateGlobalStateData } from "./global-state/update-global-state-data"; +import { hasLanguagesDataChanged } from "./languages/has-languages-data-changed"; import { logError } from "./logger/logger"; import { setStatusBarItem } from "./status-bar/set-status-bar-item"; import { calculateTime } from "./time/calculate-time"; @@ -35,7 +37,7 @@ export const periodicSyncData = async ( const timeSpentPerLanguageToday = Object.entries(filesDataToUpsert).reduce( (acc, [, { elapsedTime, languageSlug }]) => { - acc[languageSlug] = (acc[languageSlug] || 0) + elapsedTime; + acc[languageSlug] = (acc[languageSlug] ?? 0) + elapsedTime; return acc; }, {} as { [languageSlug: string]: number }, @@ -83,36 +85,52 @@ export const periodicSyncData = async ( } } - const upsertedLanguagesData = await trpc.extension.upsertLanguages.mutate({ - targetedDate: todaysDateString, - timeSpentOnDay: timeSpentToday, - timeSpentPerLanguage: timeSpentPerLanguageToday, - }); - - timeSpentOnDay = upsertedLanguagesData.timeSpentOnDay; - timeSpentPerLanguage = upsertedLanguagesData.languages; - - const files = await trpc.extension.upsertFiles.mutate({ - filesData: todayFilesData, - targetedDate: todaysDateString, - }); - updateFilesDataAfterSync(files); - - isServerSynced = true; - lastServerSync = new Date(); - - // Remove all the data for days previous to today - they've been synced - await updateGlobalStateData({ - lastServerSync, - dailyData: { - [todaysDateString]: { - timeSpentOnDay, - timeSpentPerLanguage, - dayFilesData: files, - updatedAt: new Date(), + const hasLanguagesDataChangedSinceLastSync = hasLanguagesDataChanged( + globalStateData.dailyData[todaysDateString]?.timeSpentPerLanguage ?? {}, + timeSpentPerLanguageToday, + ); + + if (hasLanguagesDataChangedSinceLastSync) { + const upsertedLanguagesData = await trpc.extension.upsertLanguages.mutate( + { + targetedDate: todaysDateString, + timeSpentOnDay: timeSpentToday, + timeSpentPerLanguage: timeSpentPerLanguageToday, + }, + ); + + timeSpentOnDay = upsertedLanguagesData.timeSpentOnDay; + timeSpentPerLanguage = upsertedLanguagesData.languages; + } + + const hasFilesDataChangedSinceLastSync = hasFilesDataChanged( + globalStateData.dailyData[todaysDateString]?.dayFilesData ?? {}, + todayFilesData, + ); + + if (hasFilesDataChangedSinceLastSync) { + const files = await trpc.extension.upsertFiles.mutate({ + filesData: todayFilesData, + targetedDate: todaysDateString, + }); + updateFilesDataAfterSync(files); + + isServerSynced = true; + lastServerSync = new Date(); + + // Remove all the data for days previous to today - they've been synced + await updateGlobalStateData({ + lastServerSync, + dailyData: { + [todaysDateString]: { + timeSpentOnDay, + timeSpentPerLanguage, + dayFilesData: files, + updatedAt: new Date(), + }, }, - }, - }); + }); + } } catch (error) { if (isTRPCClientError(error)) { logError( diff --git a/apps/vscode-extension/src/utils/time/is-new-day-handler.ts b/apps/vscode-extension/src/utils/time/is-new-day-handler.ts index 919aff24..18e6582d 100644 --- a/apps/vscode-extension/src/utils/time/is-new-day-handler.ts +++ b/apps/vscode-extension/src/utils/time/is-new-day-handler.ts @@ -10,7 +10,7 @@ export const isNewDayHandler = async ( ) => { const todaysDateString = getLocaleDate(new Date()); - // if the global state doesn't have that date, it means it a new day + // if the global state doesn't have that date, it means it is a new day if (!Object.hasOwn(dailyData, todaysDateString)) { deleteFilesDataContent(); From 05a9a1fe2bf36ddba850638a5ca67dd9bf3fdb8b Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Tue, 7 Jul 2026 01:29:00 +0200 Subject: [PATCH 7/8] ci: checks in pull request - made the `build-and-deploy` ci script run some steps as checks on a PR on the main branch: all the setup job, and the build job but neither the login step nor the push of the built images step to docker hub --- .github/workflows/build-and-deploy.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-deploy.yaml b/.github/workflows/build-and-deploy.yaml index 3ab09b20..008e92b2 100644 --- a/.github/workflows/build-and-deploy.yaml +++ b/.github/workflows/build-and-deploy.yaml @@ -3,6 +3,8 @@ name: MoonCode CI/CD Pipeline - Build api and web app docker images and deploy t on: push: branches: "main" + pull_request: + branches: "main" workflow_dispatch: jobs: @@ -53,6 +55,9 @@ jobs: uses: actions/checkout@v6 - name: Log in to Docker Hub + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -69,7 +74,7 @@ jobs: context: . file: ${{ matrix.dockerfile }} platforms: linux/amd64 - push: true + push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }} tags: | ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }}:${{ github.sha }} ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }}:latest From 690c75b344322615e393fc47934f892c37ca1583 Mon Sep 17 00:00:00 2001 From: Friedrich482 Date: Tue, 7 Jul 2026 01:38:05 +0200 Subject: [PATCH 8/8] chore: extension version - bumped the extension version to `v0.0.69` --- apps/vscode-extension/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode-extension/package.json b/apps/vscode-extension/package.json index 87d6576f..6152ce75 100644 --- a/apps/vscode-extension/package.json +++ b/apps/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "mooncode", "displayName": "MoonCode", "description": "MoonCode is an extension that tracks your coding time (like WakaTime) and gives you a detailed summary about all your coding statistics. With MoonCode, developers get the full history of their coding activity.", - "version": "0.0.68", + "version": "0.0.69", "icon": "./public/moon.png", "publisher": "Friedrich482", "author": {