Add version display in admin sidebar footer

- Add /api/version endpoint that returns build info from env vars
- Add version footer to Layout.tsx showing build version, git SHA, and image tag
- Update Dockerfile to accept build args for version info (APP_BUILD_VERSION, APP_GIT_SHA, APP_BUILD_TIME, CONTAINER_IMAGE_TAG)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-11-30 10:01:10 -07:00
parent afc71e4225
commit 0083f6a510
4 changed files with 73 additions and 1 deletions

View File

@@ -12,6 +12,18 @@ RUN npm run build
# Production stage
FROM node:20-slim
# Build arguments for version info
ARG APP_BUILD_VERSION=dev
ARG APP_GIT_SHA=unknown
ARG APP_BUILD_TIME=unknown
ARG CONTAINER_IMAGE_TAG=local
# Set version info as environment variables
ENV APP_BUILD_VERSION=${APP_BUILD_VERSION}
ENV APP_GIT_SHA=${APP_GIT_SHA}
ENV APP_BUILD_TIME=${APP_BUILD_TIME}
ENV CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}
# Install Chromium dependencies
RUN apt-get update && apt-get install -y \
chromium \

View File

@@ -51,6 +51,7 @@ import apiPermissionsRoutes from './routes/api-permissions';
import parallelScrapeRoutes from './routes/parallel-scrape';
import scheduleRoutes from './routes/schedule';
import crawlerSandboxRoutes from './routes/crawler-sandbox';
import versionRoutes from './routes/version';
import { trackApiUsage, checkRateLimit } from './middleware/apiTokenTracker';
import { startCrawlScheduler } from './services/crawl-scheduler';
import { validateWordPressPermissions } from './middleware/wordpressPermissions';
@@ -80,6 +81,7 @@ app.use('/api/api-permissions', apiPermissionsRoutes);
app.use('/api/parallel-scrape', parallelScrapeRoutes);
app.use('/api/schedule', scheduleRoutes);
app.use('/api/crawler-sandbox', crawlerSandboxRoutes);
app.use('/api/version', versionRoutes);
async function startServer() {
try {

View File

@@ -0,0 +1,25 @@
import { Router, Request, Response } from 'express';
const router = Router();
/**
* GET /api/version
* Returns build version information for display in admin UI
*/
router.get('/', async (req: Request, res: Response) => {
try {
const versionInfo = {
build_version: process.env.APP_BUILD_VERSION || 'dev',
git_sha: process.env.APP_GIT_SHA || 'local',
build_time: process.env.APP_BUILD_TIME || new Date().toISOString(),
image_tag: process.env.CONTAINER_IMAGE_TAG || 'local',
};
res.json(versionInfo);
} catch (error: any) {
console.error('Error fetching version info:', error);
res.status(500).json({ error: 'Failed to fetch version info' });
}
});
export default router;

View File

@@ -1,6 +1,7 @@
import { ReactNode } from 'react';
import { ReactNode, useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { api } from '../lib/api';
import {
LayoutDashboard,
Store,
@@ -24,6 +25,13 @@ interface LayoutProps {
children: ReactNode;
}
interface VersionInfo {
build_version: string;
git_sha: string;
build_time: string;
image_tag: string;
}
interface NavLinkProps {
to: string;
icon: ReactNode;
@@ -69,6 +77,19 @@ export function Layout({ children }: LayoutProps) {
const navigate = useNavigate();
const location = useLocation();
const { user, logout } = useAuthStore();
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
useEffect(() => {
const fetchVersion = async () => {
try {
const response = await api.get('/version');
setVersionInfo(response.data);
} catch (error) {
console.error('Failed to fetch version info:', error);
}
};
fetchVersion();
}, []);
const handleLogout = () => {
logout();
@@ -206,6 +227,18 @@ export function Layout({ children }: LayoutProps) {
<span>Logout</span>
</button>
</div>
{/* Version Footer */}
{versionInfo && (
<div className="px-3 py-2 border-t border-gray-200 bg-gray-50">
<p className="text-xs text-gray-500 text-center">
{versionInfo.build_version} ({versionInfo.git_sha.slice(0, 7)})
</p>
<p className="text-xs text-gray-400 text-center mt-0.5">
{versionInfo.image_tag}
</p>
</div>
)}
</div>
{/* Main Content */}