feat: add version information component and integrate version check in health API

This commit is contained in:
Arunavo Ray
2025-05-22 18:51:11 +05:30
parent 0c596ac241
commit 309f8c4341
4 changed files with 121 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { links } from "@/data/Sidebar"; import { links } from "@/data/Sidebar";
import { VersionInfo } from "./VersionInfo";
interface SidebarProps { interface SidebarProps {
className?: string; className?: string;
@@ -19,7 +20,7 @@ export function Sidebar({ className }: SidebarProps) {
return ( return (
<aside className={cn("w-64 border-r bg-background", className)}> <aside className={cn("w-64 border-r bg-background", className)}>
<div className="flex flex-col h-full py-4"> <div className="flex flex-col h-full pt-4">
<nav className="flex flex-col gap-y-1 pl-2 pr-3"> <nav className="flex flex-col gap-y-1 pl-2 pr-3">
{links.map((link, index) => { {links.map((link, index) => {
const isActive = currentPath === link.href; const isActive = currentPath === link.href;
@@ -59,6 +60,7 @@ export function Sidebar({ className }: SidebarProps) {
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
</a> </a>
</div> </div>
<VersionInfo />
</div> </div>
</div> </div>
</aside> </aside>

View File

@@ -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 (
<div className="text-xs text-muted-foreground text-center pt-2 pb-3 border-t border-border mt-2">
{versionInfo.updateAvailable ? (
<div className="flex flex-col">
<span>v{versionInfo.current}</span>
<span className="text-primary">v{versionInfo.latest} available</span>
</div>
) : (
<span>v{versionInfo.current}</span>
)}
</div>
);
}

View File

@@ -94,6 +94,8 @@ export interface HealthResponse {
status: "ok" | "error"; status: "ok" | "error";
timestamp: string; timestamp: string;
version: string; version: string;
latestVersion: string;
updateAvailable: boolean;
database: { database: {
connected: boolean; connected: boolean;
message: string; message: string;
@@ -147,6 +149,8 @@ export const healthApi = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : "Unknown error checking health", error: error instanceof Error ? error.message : "Unknown error checking health",
version: "unknown", version: "unknown",
latestVersion: "unknown",
updateAvailable: false,
database: { connected: false, message: "Failed to connect to API" }, database: { connected: false, message: "Failed to connect to API" },
system: { system: {
uptime: { startTime: "", uptimeMs: 0, formatted: "N/A" }, uptime: { startTime: "", uptimeMs: 0, formatted: "N/A" },

View File

@@ -3,10 +3,20 @@ import { jsonResponse } from "@/lib/utils";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { ENV } from "@/lib/config"; import { ENV } from "@/lib/config";
import os from "os"; import os from "os";
import axios from "axios";
// Track when the server started // Track when the server started
const serverStartTime = new Date(); 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 () => { export const GET: APIRoute = async () => {
try { try {
// Check database connection by running a simple query // Check database connection by running a simple query
@@ -24,11 +34,19 @@ export const GET: APIRoute = async () => {
env: ENV.NODE_ENV, env: ENV.NODE_ENV,
}; };
// Get current and latest versions
const currentVersion = process.env.npm_package_version || "unknown";
const latestVersion = await checkLatestVersion();
// Build response // Build response
const healthData = { const healthData = {
status: "ok", status: "ok",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "unknown", version: currentVersion,
latestVersion: latestVersion,
updateAvailable: latestVersion !== "unknown" &&
currentVersion !== "unknown" &&
latestVersion !== currentVersion,
database: dbStatus, database: dbStatus,
system: systemInfo, system: systemInfo,
}; };
@@ -45,6 +63,9 @@ export const GET: APIRoute = async () => {
status: "error", status: "error",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : "Unknown error", error: error instanceof Error ? error.message : "Unknown error",
version: process.env.npm_package_version || "unknown",
latestVersion: "unknown",
updateAvailable: false,
}, },
status: 503, // Service Unavailable status: 503, // Service Unavailable
}); });
@@ -122,5 +143,37 @@ function formatBytes(bytes: number): string {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
} }
/**
* Check for the latest version from GitHub releases
*/
async function checkLatestVersion(): Promise<string> {
// 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 tag for raw SQL queries
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";