feat(devops): add /devops page with platform common devops package

- Add @bytelyst/devops backend endpoints to devops backend
- Add /api/devops/version (public) and /api/devops/info (admin) endpoints
- Add /devops page using @bytelyst/devops/ui DevopsPanel component
- Add devops link to sidebar navigation
- Add build metadata and runtime information display
- Follow trading web devops pattern

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
root 2026-05-11 03:37:44 +00:00
parent fbaaa71a66
commit 788794b740
5 changed files with 127 additions and 1 deletions

View File

@ -27,7 +27,8 @@
"@azure/identity": "^4.5.0",
"@azure/keyvault-secrets": "^4.9.0",
"@azure/cosmos": "^4.1.0",
"dotenv": "^16.4.5"
"dotenv": "^16.4.5",
"@bytelyst/devops": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.0.3",

View File

@ -3,6 +3,7 @@ import { config } from './lib/config.js';
import { initializeContainers } from './lib/cosmos-init.js';
import { extractAuth, AuthError } from './lib/auth.js';
import { generateCsrfToken, validateCsrfToken, getSessionId } from './lib/csrf.js';
import { collectDevopsInfo, getBuildInfo, httpDependencyCheck, readServiceVersion } from '@bytelyst/devops/server';
import { serviceRoutes } from './modules/services/routes.js';
import { deploymentRoutes } from './modules/deployments/routes.js';
import { healthRoutes } from './modules/health/routes.js';
@ -190,6 +191,14 @@ fastify.options('*', async (request, reply) => {
// Health check
fastify.get('/health', async () => ({ status: 'ok', service: 'devops-backend' }));
// Admin check helper
async function requireAdmin(request: any) {
const role = request.authRole;
if (role !== 'admin') {
throw new Error('Admin access required');
}
}
// Register standalone routes with /api prefix
await fastify.register(async function (fastify) {
// Performance metrics endpoint (admin only) - DEPRECATED: Use /api/system/metrics instead
@ -255,6 +264,34 @@ await fastify.register(async function (fastify) {
return reply.send({ message: 'Seeded default services' });
});
// DevOps version endpoint (public - no auth required)
fastify.get('/devops/version', async (request, reply) => {
return reply.send(getBuildInfo());
});
// DevOps info endpoint (admin only)
fastify.get('/devops/info', {
preHandler: async (req) => requireAdmin(req),
}, async (request, reply) => {
try {
const info = await collectDevopsInfo({
productId: config.PRODUCT_ID || 'devops',
serviceName: 'devops-backend',
serviceVersion: readServiceVersion(import.meta.url),
dependencyChecks: [
() => httpDependencyCheck('platform-service', `${config.PLATFORM_URL}/health`),
],
extra: {
devopsApiUrl: config.DEVOPS_API_URL,
},
});
return reply.send(info);
} catch (error: any) {
fastify.log.error('Failed to collect devops info:', error);
return reply.code(500).send({ error: error.message });
}
});
}, { prefix: '/api' });
// Register modular routes with /api prefix

View File

@ -15,6 +15,7 @@
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@bytelyst/devops": "^0.1.3",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"next": "16.0.0",

View File

@ -0,0 +1,85 @@
'use client';
import { DevopsPanel, type DevopsInfo } from '@bytelyst/devops/ui';
import { devopsApiUrl } from '@/lib/product-config';
import { getAccessToken } from '@/lib/api';
const bundleStartTime = Date.now();
async function fetchBackendInfo(): Promise<DevopsInfo> {
const token = await getAccessToken();
const res = await fetch(`${devopsApiUrl}/api/devops/info`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(body?.error ?? `Backend devops info failed (${res.status})`);
}
return (await res.json()) as DevopsInfo;
}
async function fetchWebInfo(): Promise<DevopsInfo> {
const env = process.env as Record<string, string | undefined>;
const builtAt = env.NEXT_PUBLIC_BYTELYST_BUILT_AT || null;
const startedAtMs = bundleStartTime;
const uptimeSec = Math.floor((Date.now() - startedAtMs) / 1000);
return {
build: {
commitSha: env.NEXT_PUBLIC_BYTELYST_COMMIT_SHA || null,
commitShaFull: env.NEXT_PUBLIC_BYTELYST_COMMIT_SHA_FULL || null,
branch: env.NEXT_PUBLIC_BYTELYST_BRANCH || null,
builtAt,
commitAuthor: env.NEXT_PUBLIC_BYTELYST_COMMIT_AUTHOR || null,
commitMessage: env.NEXT_PUBLIC_BYTELYST_COMMIT_MESSAGE || null,
dockerImage: env.NEXT_PUBLIC_BYTELYST_DOCKER_IMAGE || null,
},
runtime: {
uptimeSeconds: uptimeSec,
uptimeHuman: humanizeUptime(uptimeSec),
nodeVersion: 'browser',
platform: typeof window !== 'undefined' ? navigator.platform || 'unknown' : 'unknown',
arch: typeof window !== 'undefined' && navigator.userAgent.includes('arm') ? 'arm' : 'x86',
pid: 0,
hostname: typeof window !== 'undefined' ? window.location.hostname : 'unknown',
memoryMb: Math.round(((performance as any)?.memory?.usedJSHeapSize ?? 0) / 1024 / 1024),
heapMb: Math.round(((performance as any)?.memory?.usedJSHeapSize ?? 0) / 1024 / 1024),
startedAt: new Date(startedAtMs).toISOString(),
},
config: {
productId: env.NEXT_PUBLIC_PRODUCT_ID || 'devops',
serviceName: 'devops-web',
serviceVersion: '1.0.0',
nodeEnv: env.NODE_ENV || 'production',
envKeys: Object.keys(env)
.filter((k) => /^NEXT_PUBLIC_/.test(k) && !/SECRET|KEY|TOKEN|PASSWORD/i.test(k))
.sort(),
},
extra: {
devopsApiUrl,
userAgent: typeof window !== 'undefined' ? navigator.userAgent : 'unknown',
},
};
}
function humanizeUptime(seconds: number): string {
if (seconds < 60) return `${seconds}s`;
const mins = Math.floor(seconds / 60);
if (mins < 60) return `${mins}m ${seconds % 60}s`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ${mins % 60}m`;
const days = Math.floor(hrs / 24);
return `${days}d ${hrs % 24}h ${mins % 60}m`;
}
export default function DevOpsPage() {
return (
<div className="p-8 max-md:p-4">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">DevOps</h1>
<p className="text-sm text-gray-600">System information and deployment details</p>
</div>
<DevopsPanel fetchInfo={fetchBackendInfo} fetchWebInfo={fetchWebInfo} />
</div>
);
}

View File

@ -17,6 +17,7 @@ import {
Sun,
Moon,
HeartPulse,
Server,
} from 'lucide-react';
import { useAuth } from '@/lib/auth';
@ -27,6 +28,7 @@ const navItems = [
{ href: '/system', label: 'System', icon: Cpu },
{ href: '/env', label: 'Environment', icon: Key },
{ href: '/code-quality', label: 'Code Quality', icon: Code2 },
{ href: '/devops', label: 'DevOps', icon: Server },
{ href: '/settings/cosmos', label: 'Settings', icon: Settings },
];