feat: show backend status and system version
This commit is contained in:
parent
0e60b06e05
commit
8e5d29c7a3
5 changed files with 147 additions and 0 deletions
|
|
@ -1,10 +1,12 @@
|
||||||
import { RouterProvider } from "react-router-dom";
|
import { RouterProvider } from "react-router-dom";
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
import { AuthProvider } from "./contexts/AuthContext";
|
import { AuthProvider } from "./contexts/AuthContext";
|
||||||
|
import { SystemStatusBadge } from "./components/SystemStatusBadge";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<SystemStatusBadge />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
112
frontend/src/components/SystemStatusBadge.tsx
Normal file
112
frontend/src/components/SystemStatusBadge.tsx
Normal file
|
|
@ -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<LoadState>("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 (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-4 right-4 z-[70]"
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center overflow-hidden rounded-2xl border border-border-soft bg-surface/95 shadow-[0_12px_30px_rgba(79,62,35,0.16)] backdrop-blur-xl transition-all duration-300",
|
||||||
|
isExpanded ? "max-w-xs pl-2 pr-3 py-2" : "w-12 h-12 p-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Mostrar status do sistema"
|
||||||
|
onClick={() => setIsPinnedOpen((current) => !current)}
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 w-8 shrink-0 items-center justify-center rounded-xl transition-transform duration-200 hover:scale-105",
|
||||||
|
isOnline ? "bg-success-soft text-success" : "bg-danger-soft text-danger",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isOnline ? <Activity size={16} /> : <WifiOff size={16} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid transition-all duration-300",
|
||||||
|
isExpanded ? "ml-3 grid-cols-[1fr] opacity-100" : "ml-0 grid-cols-[0fr] opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="min-w-0 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[11px] font-black uppercase tracking-[0.22em] text-text-muted">
|
||||||
|
Sistema
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-2 py-0.5 text-[10px] font-black uppercase tracking-wide",
|
||||||
|
backendStatus === "loading" && "bg-warning-soft text-warning",
|
||||||
|
backendStatus === "online" && "bg-success-soft text-success",
|
||||||
|
backendStatus === "offline" && "bg-danger-soft text-danger",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{backendStatus === "loading" ? "Verificando" : isOnline ? "Online" : "Offline"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1 flex items-center gap-2 text-[11px] font-medium text-text-muted">
|
||||||
|
<span className="whitespace-nowrap">API {backendVersion}</span>
|
||||||
|
<span className="text-border-strong">•</span>
|
||||||
|
<span className="whitespace-nowrap">WEB {frontendVersion}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
frontend/src/services/statusService.ts
Normal file
20
frontend/src/services/statusService.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { apiFetch } from "../lib/api";
|
||||||
|
|
||||||
|
type HealthData = Record<string, string | number | boolean | null>;
|
||||||
|
|
||||||
|
type HealthCheck = {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
data?: HealthData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackendHealthResponse = {
|
||||||
|
status: string;
|
||||||
|
checks?: HealthCheck[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getBackendStatus() {
|
||||||
|
return apiFetch<BackendHealthResponse>("/status", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
9
frontend/src/vite-env.d.ts
vendored
Normal file
9
frontend/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_VERSION: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import pkg from './package.json'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue