From 788794b740c58ca6f9e100dad8a6aebf4a1fe761 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 11 May 2026 03:37:44 +0000 Subject: [PATCH] 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> --- dashboard/backend/package.json | 3 +- dashboard/backend/src/server.ts | 37 +++++++++ dashboard/web/package.json | 1 + dashboard/web/src/app/devops/page.tsx | 85 ++++++++++++++++++++ dashboard/web/src/components/sidebar-nav.tsx | 2 + 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 dashboard/web/src/app/devops/page.tsx diff --git a/dashboard/backend/package.json b/dashboard/backend/package.json index 445268c..48082fd 100644 --- a/dashboard/backend/package.json +++ b/dashboard/backend/package.json @@ -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", diff --git a/dashboard/backend/src/server.ts b/dashboard/backend/src/server.ts index 2c59ab6..a877221 100644 --- a/dashboard/backend/src/server.ts +++ b/dashboard/backend/src/server.ts @@ -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 diff --git a/dashboard/web/package.json b/dashboard/web/package.json index d29aa9f..924e29e 100644 --- a/dashboard/web/package.json +++ b/dashboard/web/package.json @@ -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", diff --git a/dashboard/web/src/app/devops/page.tsx b/dashboard/web/src/app/devops/page.tsx new file mode 100644 index 0000000..e52d77a --- /dev/null +++ b/dashboard/web/src/app/devops/page.tsx @@ -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 { + 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 { + const env = process.env as Record; + 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 ( +
+
+

DevOps

+

System information and deployment details

+
+ +
+ ); +} diff --git a/dashboard/web/src/components/sidebar-nav.tsx b/dashboard/web/src/components/sidebar-nav.tsx index a56a4cb..2716015 100644 --- a/dashboard/web/src/components/sidebar-nav.tsx +++ b/dashboard/web/src/components/sidebar-nav.tsx @@ -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 }, ];