diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6556555..4efad5a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,12 @@ import { RouterProvider } from "react-router-dom"; import { router } from "./router"; import { AuthProvider } from "./contexts/AuthContext"; +import { SystemStatusBadge } from "./components/SystemStatusBadge"; function App() { return ( + ); diff --git a/frontend/src/components/SystemStatusBadge.tsx b/frontend/src/components/SystemStatusBadge.tsx new file mode 100644 index 0000000..01704a9 --- /dev/null +++ b/frontend/src/components/SystemStatusBadge.tsx @@ -0,0 +1,112 @@ +import { Activity, WifiOff } from "lucide-react"; +import { useEffect, useState } from "react"; + +import { cn } from "../lib/utils"; +import { getBackendStatus, type BackendHealthResponse } from "../services/statusService"; + +type LoadState = "loading" | "online" | "offline"; + +function extractBackendVersion(payload: BackendHealthResponse | null) { + const versionCheck = payload?.checks?.find((check) => check.name === "backend-version"); + const version = versionCheck?.data?.version; + + return typeof version === "string" ? version : "unknown"; +} + +export function SystemStatusBadge() { + const [backendStatus, setBackendStatus] = useState("loading"); + const [backendVersion, setBackendVersion] = useState("..."); + const [isPinnedOpen, setIsPinnedOpen] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + useEffect(() => { + let active = true; + + async function loadStatus() { + try { + const response = await getBackendStatus(); + if (!active) return; + + setBackendStatus(response.status === "UP" ? "online" : "offline"); + setBackendVersion(extractBackendVersion(response)); + } catch { + if (!active) return; + + setBackendStatus("offline"); + setBackendVersion("indisponivel"); + } + } + + void loadStatus(); + const intervalId = window.setInterval(() => { + void loadStatus(); + }, 30000); + + return () => { + active = false; + window.clearInterval(intervalId); + }; + }, []); + + const frontendVersion = import.meta.env.VITE_APP_VERSION; + const isOnline = backendStatus === "online"; + const isExpanded = isHovered || isPinnedOpen; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ + +
+
+
+ + Sistema + + + {backendStatus === "loading" ? "Verificando" : isOnline ? "Online" : "Offline"} + +
+ +
+ API {backendVersion} + • + WEB {frontendVersion} +
+
+
+
+
+ ); +} diff --git a/frontend/src/services/statusService.ts b/frontend/src/services/statusService.ts new file mode 100644 index 0000000..beb8ad8 --- /dev/null +++ b/frontend/src/services/statusService.ts @@ -0,0 +1,20 @@ +import { apiFetch } from "../lib/api"; + +type HealthData = Record; + +type HealthCheck = { + name: string; + status: string; + data?: HealthData; +}; + +export type BackendHealthResponse = { + status: string; + checks?: HealthCheck[]; +}; + +export async function getBackendStatus() { + return apiFetch("/status", { + method: "GET", + }); +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..4c755bf --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_VERSION: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 0906f3c..cc44d1b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,8 +1,12 @@ import tailwindcss from '@tailwindcss/vite' import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' +import pkg from './package.json' export default defineConfig({ + define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version), + }, plugins: [react(), tailwindcss()], server: { proxy: {