diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 774b1ab..43d0626 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; import { ExternalLink } from "lucide-react"; import { links } from "@/data/Sidebar"; +import { VersionInfo } from "./VersionInfo"; interface SidebarProps { className?: string; @@ -19,7 +20,7 @@ export function Sidebar({ className }: SidebarProps) { return ( diff --git a/src/components/layout/VersionInfo.tsx b/src/components/layout/VersionInfo.tsx new file mode 100644 index 0000000..be6e7ae --- /dev/null +++ b/src/components/layout/VersionInfo.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import { healthApi } from "@/lib/api"; + +export function VersionInfo() { + const [versionInfo, setVersionInfo] = useState<{ + current: string; + latest: string; + updateAvailable: boolean; + }>({ + current: "loading...", + latest: "", + updateAvailable: false + }); + + useEffect(() => { + const fetchVersion = async () => { + try { + const healthData = await healthApi.check(); + setVersionInfo({ + current: healthData.version || "unknown", + latest: healthData.latestVersion || "unknown", + updateAvailable: healthData.updateAvailable || false + }); + } catch (error) { + console.error("Failed to fetch version:", error); + setVersionInfo({ + current: "unknown", + latest: "", + updateAvailable: false + }); + } + }; + + fetchVersion(); + }, []); + + return ( +
+ {versionInfo.updateAvailable ? ( +
+ v{versionInfo.current} + v{versionInfo.latest} available +
+ ) : ( + v{versionInfo.current} + )} +
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 9526095..acf7b73 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -94,6 +94,8 @@ export interface HealthResponse { status: "ok" | "error"; timestamp: string; version: string; + latestVersion: string; + updateAvailable: boolean; database: { connected: boolean; message: string; @@ -147,6 +149,8 @@ export const healthApi = { timestamp: new Date().toISOString(), error: error instanceof Error ? error.message : "Unknown error checking health", version: "unknown", + latestVersion: "unknown", + updateAvailable: false, database: { connected: false, message: "Failed to connect to API" }, system: { uptime: { startTime: "", uptimeMs: 0, formatted: "N/A" }, diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts index 9695293..022a6b6 100644 --- a/src/pages/api/health.ts +++ b/src/pages/api/health.ts @@ -3,15 +3,25 @@ import { jsonResponse } from "@/lib/utils"; import { db } from "@/lib/db"; import { ENV } from "@/lib/config"; import os from "os"; +import axios from "axios"; // Track when the server started const serverStartTime = new Date(); +// Cache for the latest version to avoid frequent GitHub API calls +interface VersionCache { + latestVersion: string; + timestamp: number; +} + +let versionCache: VersionCache | null = null; +const CACHE_TTL = 3600000; // 1 hour in milliseconds + export const GET: APIRoute = async () => { try { // Check database connection by running a simple query const dbStatus = await checkDatabaseConnection(); - + // Get system information const systemInfo = { uptime: getUptime(), @@ -23,28 +33,39 @@ export const GET: APIRoute = async () => { }, env: ENV.NODE_ENV, }; - + + // Get current and latest versions + const currentVersion = process.env.npm_package_version || "unknown"; + const latestVersion = await checkLatestVersion(); + // Build response const healthData = { status: "ok", timestamp: new Date().toISOString(), - version: process.env.npm_package_version || "unknown", + version: currentVersion, + latestVersion: latestVersion, + updateAvailable: latestVersion !== "unknown" && + currentVersion !== "unknown" && + latestVersion !== currentVersion, database: dbStatus, system: systemInfo, }; - + return jsonResponse({ data: healthData, status: 200, }); } catch (error) { console.error("Health check failed:", error); - + return jsonResponse({ data: { status: "error", timestamp: new Date().toISOString(), error: error instanceof Error ? error.message : "Unknown error", + version: process.env.npm_package_version || "unknown", + latestVersion: "unknown", + updateAvailable: false, }, status: 503, // Service Unavailable }); @@ -58,14 +79,14 @@ async function checkDatabaseConnection() { try { // Run a simple query to check if the database is accessible const result = await db.select({ test: sql`1` }).from(sql`sqlite_master`).limit(1); - + return { connected: true, message: "Database connection successful", }; } catch (error) { console.error("Database connection check failed:", error); - + return { connected: false, message: error instanceof Error ? error.message : "Database connection failed", @@ -79,13 +100,13 @@ async function checkDatabaseConnection() { function getUptime() { const now = new Date(); const uptimeMs = now.getTime() - serverStartTime.getTime(); - + // Convert to human-readable format const seconds = Math.floor(uptimeMs / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); - + return { startTime: serverStartTime.toISOString(), uptimeMs, @@ -98,7 +119,7 @@ function getUptime() { */ function getMemoryUsage() { const memoryUsage = process.memoryUsage(); - + return { rss: formatBytes(memoryUsage.rss), heapTotal: formatBytes(memoryUsage.heapTotal), @@ -114,13 +135,45 @@ function getMemoryUsage() { */ function formatBytes(bytes: number): string { if (bytes === 0) return '0 Bytes'; - + const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); - + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } +/** + * Check for the latest version from GitHub releases + */ +async function checkLatestVersion(): Promise { + // Return cached version if available and not expired + if (versionCache && (Date.now() - versionCache.timestamp) < CACHE_TTL) { + return versionCache.latestVersion; + } + + try { + // Fetch the latest release from GitHub + const response = await axios.get( + 'https://api.github.com/repos/arunavo4/gitea-mirror/releases/latest', + { headers: { 'Accept': 'application/vnd.github.v3+json' } } + ); + + // Extract version from tag_name (remove 'v' prefix if present) + const latestVersion = response.data.tag_name.replace(/^v/, ''); + + // Update cache + versionCache = { + latestVersion, + timestamp: Date.now() + }; + + return latestVersion; + } catch (error) { + console.error('Failed to check for latest version:', error); + return 'unknown'; + } +} + // Import sql tag for raw SQL queries import { sql } from "drizzle-orm";