From ec1b6c9084477e320b4db0aaaa98102f8d06cac4 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:59:59 +0800 Subject: [PATCH 1/2] feat: Implement backend health check with an animated waiting screen when the service is unavailable. --- frontend/bun.lock | 9 ++ frontend/package.json | 1 + frontend/src/api/system.ts | 17 ++ .../valuecell/backend-health-check.tsx | 147 ++++++++++++++++++ frontend/src/root.tsx | 28 ++-- 5 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 frontend/src/api/system.ts create mode 100644 frontend/src/components/valuecell/backend-health-check.tsx diff --git a/frontend/bun.lock b/frontend/bun.lock index 1d756c499..e2195764f 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -32,6 +32,7 @@ "cmdk": "^1.1.1", "dayjs": "^1.11.18", "echarts": "^6.0.0", + "framer-motion": "^12.23.24", "isbot": "5.1.31", "lucide-react": "^0.552.0", "mutative": "^1.3.0", @@ -727,6 +728,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="], + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], @@ -1033,6 +1036,10 @@ "morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="], + "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], + + "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msw": ["msw@2.11.3", "", { "dependencies": { "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.39.1", "@open-draft/deferred-promise": "^2.2.0", "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", "graphql": "^16.8.1", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^4.26.1", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-878imp8jxIpfzuzxYfX0qqTq1IFQz/1/RBHs/PyirSjzi+xKM/RRfIpIqHSCWjH0GxidrjhgiiXC+DWXNDvT9w=="], @@ -1515,6 +1522,8 @@ "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "framer-motion/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], "hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], diff --git a/frontend/package.json b/frontend/package.json index 4acbd4766..0e8e27634 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,6 +45,7 @@ "cmdk": "^1.1.1", "dayjs": "^1.11.18", "echarts": "^6.0.0", + "framer-motion": "^12.23.24", "isbot": "5.1.31", "lucide-react": "^0.552.0", "mutative": "^1.3.0", diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts new file mode 100644 index 000000000..10c679c63 --- /dev/null +++ b/frontend/src/api/system.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { apiClient } from "@/lib/api-client"; + +export const useBackendHealth = () => { + return useQuery({ + queryKey: ["backend-health"], + queryFn: () => + apiClient.get("/health", { + requiresAuth: false, + }), + retry: false, + refetchInterval: (query) => { + return query.state.status === "error" ? 2000 : 10000; + }, + refetchOnWindowFocus: true, + }); +}; diff --git a/frontend/src/components/valuecell/backend-health-check.tsx b/frontend/src/components/valuecell/backend-health-check.tsx new file mode 100644 index 000000000..fbfeeb29a --- /dev/null +++ b/frontend/src/components/valuecell/backend-health-check.tsx @@ -0,0 +1,147 @@ +import { AnimatePresence, motion } from "framer-motion"; +import { RefreshCw, ServerCrash, WifiOff } from "lucide-react"; +import type React from "react"; +import { useEffect, useState } from "react"; +import { useBackendHealth } from "@/api/system"; +import { Button } from "@/components/ui/button"; + +export function BackendHealthCheck({ + children, +}: { + children: React.ReactNode; +}) { + const { isError, refetch, isFetching, isSuccess } = useBackendHealth(); + const [showError, setShowError] = useState(false); + + // Debounce showing the error screen to avoid flickering on initial load or brief network blips + useEffect(() => { + let timer: ReturnType; + if (isError) { + timer = setTimeout(() => setShowError(true), 500); + } else { + setShowError(false); + } + return () => clearTimeout(timer); + }, [isError]); + + if (isSuccess && !showError) { + return <>{children}; + } + + return ( + <> + {/* We can render children hidden or not at all. + If we want to block initialization completely, we don't render children. + If we want to keep the app mounted but covered, we render children. + Given the requirement "normal routing requests should not be sent out", + we should NOT render children when in error state. + */} + {/* However, for initial load, we might want to show a loading state or just wait. + If we are in "loading" state (initial fetch), we might want to show a spinner or nothing. + If we are in "error" state, we show the error screen. + */} + + + {showError && ( + +
+ {/* Animated Icon Container */} +
+ + + + + {/* Orbiting dot */} + +
+ + +
+ + {/* Text Content */} + +

+ Waiting For Service +

+

+ The backend is starting or waiting to start.
+ Please wait while we attempt to connect... +

+
+ + {/* Status Indicator & Action */} + +
+ {isFetching ? ( + <> + + Attempting to reconnect... + + ) : ( + <> + + Connection lost + + )} +
+ + +
+
+ + )} + + + {/* While loading initially (and not error yet), we might want to show nothing or a splash screen. + For now, we'll just render nothing until we have success or error. + */} + + ); +} diff --git a/frontend/src/root.tsx b/frontend/src/root.tsx index d97966193..af88088dc 100644 --- a/frontend/src/root.tsx +++ b/frontend/src/root.tsx @@ -41,22 +41,26 @@ const queryClient = new QueryClient({ }, }); +import { BackendHealthCheck } from "@/components/valuecell/backend-health-check"; + export default function Root() { return ( - -
- + + +
+ -
- -
- -
-
+
+ +
+ +
+
+
); } From 03010d3816d708ea3fccd07b42e5ce295f75d876 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:23:24 +0800 Subject: [PATCH 2/2] feat: Add `/healthz` endpoint for backend health checks, update frontend API call, and fix macOS application support path. --- frontend/src/api/system.ts | 2 +- python/valuecell/server/api/app.py | 4 ++++ python/valuecell/utils/env.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index 10c679c63..0c5cd0336 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -5,7 +5,7 @@ export const useBackendHealth = () => { return useQuery({ queryKey: ["backend-health"], queryFn: () => - apiClient.get("/health", { + apiClient.get("/healthz", { requiresAuth: false, }), retry: false, diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py index a2ce87cb8..0332c68fd 100644 --- a/python/valuecell/server/api/app.py +++ b/python/valuecell/server/api/app.py @@ -199,6 +199,10 @@ async def home_page(): msg="Welcome to ValueCell Server API", ) + @app.get(f"{API_PREFIX}/healthz", response_model=SuccessResponse) + async def health_check(): + return SuccessResponse.create(msg="Welcome to ValueCell!") + # Include i18n router app.include_router(create_i18n_router(), prefix=API_PREFIX) diff --git a/python/valuecell/utils/env.py b/python/valuecell/utils/env.py index 9f2564ce9..ca6e40b74 100644 --- a/python/valuecell/utils/env.py +++ b/python/valuecell/utils/env.py @@ -24,7 +24,7 @@ def get_system_env_dir() -> Path: return base / "ValueCell" # macOS (posix with darwin kernel) if sys_platform_is_darwin(): - return home / "Library" / "Application Support" / "ValueCell" + return home / "Library" / "ApplicationSupport" / "ValueCell" # Linux and other Unix-like return home / ".config" / "valuecell"