From c39da9158886f424b66e844519b0fb71d7733267 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 11 May 2026 03:37:59 +0000 Subject: [PATCH] feat(platform): add /devops page with platform common devops package - Add @bytelyst/devops backend endpoints to platform-service - Add /api/devops/version (public) and /api/devops/info (admin) endpoints - Add /devops page to admin-web using @bytelyst/devops/ui DevopsPanel - Add devops link to admin web 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> --- dashboards/admin-web/package.json | 1 + dashboards/admin-web/src/app/devops/page.tsx | 97 +++++++++++++++++++ .../admin-web/src/components/sidebar-nav.tsx | 2 + services/platform-service/package.json | 1 + services/platform-service/src/server.ts | 51 ++++++++++ 5 files changed, 152 insertions(+) create mode 100644 dashboards/admin-web/src/app/devops/page.tsx diff --git a/dashboards/admin-web/package.json b/dashboards/admin-web/package.json index c9a72a1c..6ce72bd3 100644 --- a/dashboards/admin-web/package.json +++ b/dashboards/admin-web/package.json @@ -31,6 +31,7 @@ "@bytelyst/cosmos": "workspace:*", "@bytelyst/datastore": "workspace:*", "@bytelyst/design-tokens": "workspace:*", + "@bytelyst/devops": "workspace:*", "@bytelyst/errors": "workspace:*", "@bytelyst/extraction": "workspace:*", "@bytelyst/logger": "workspace:*", diff --git a/dashboards/admin-web/src/app/devops/page.tsx b/dashboards/admin-web/src/app/devops/page.tsx new file mode 100644 index 00000000..c5c98acf --- /dev/null +++ b/dashboards/admin-web/src/app/devops/page.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { DevopsPanel, type DevopsInfo } from '@bytelyst/devops/ui'; +import { useAuth } from '@bytelyst/react-auth'; + +const bundleStartTime = Date.now(); + +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() { + const { getAccessToken } = useAuth(); + + async function fetchBackendInfo(): Promise { + const token = getAccessToken(); + const platformUrl = process.env.NEXT_PUBLIC_PLATFORM_URL || 'http://localhost:4003'; + const res = await fetch(`${platformUrl}/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 unknown as { memory?: { usedJSHeapSize?: number } })?.memory + ?.usedJSHeapSize ?? 0) / + 1024 / + 1024 + ), + heapMb: Math.round( + ((performance as unknown as { memory?: { usedJSHeapSize?: number } })?.memory + ?.usedJSHeapSize ?? 0) / + 1024 / + 1024 + ), + startedAt: new Date(startedAtMs).toISOString(), + }, + config: { + productId: env.NEXT_PUBLIC_PRODUCT_ID || 'admin', + serviceName: 'admin-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: { + platformUrl: env.NEXT_PUBLIC_PLATFORM_URL || 'http://localhost:4003', + userAgent: typeof window !== 'undefined' ? navigator.userAgent : 'unknown', + }, + }; + } + + return ( +
+
+

DevOps

+

System information and deployment details

+
+ +
+ ); +} diff --git a/dashboards/admin-web/src/components/sidebar-nav.tsx b/dashboards/admin-web/src/components/sidebar-nav.tsx index cf1bef04..2d375cc3 100644 --- a/dashboards/admin-web/src/components/sidebar-nav.tsx +++ b/dashboards/admin-web/src/components/sidebar-nav.tsx @@ -57,6 +57,7 @@ import { Bot, Globe, Download, + Server, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAuth } from '@/lib/auth-context'; @@ -114,6 +115,7 @@ const navItems = [ { href: '/ai-diagnostics', label: 'AI Diagnostics', icon: BrainCircuit }, { href: '/feedback', label: 'User Feedback', icon: MessageSquare }, { href: '/ops/secrets', label: 'Secrets Manager', icon: KeyRound }, + { href: '/devops', label: 'DevOps', icon: Server }, { href: '/settings', label: 'Settings', icon: Settings }, ]; diff --git a/services/platform-service/package.json b/services/platform-service/package.json index e8dd2762..98dfcc59 100644 --- a/services/platform-service/package.json +++ b/services/platform-service/package.json @@ -21,6 +21,7 @@ "@bytelyst/config": "workspace:*", "@bytelyst/cosmos": "workspace:*", "@bytelyst/datastore": "workspace:*", + "@bytelyst/devops": "workspace:*", "@bytelyst/errors": "workspace:*", "@bytelyst/events": "workspace:*", "@bytelyst/fastify-core": "workspace:*", diff --git a/services/platform-service/src/server.ts b/services/platform-service/src/server.ts index 899d022d..16d3421e 100644 --- a/services/platform-service/src/server.ts +++ b/services/platform-service/src/server.ts @@ -10,6 +10,12 @@ // Resolve secrets from configured provider BEFORE config parsing import { resolveSecrets, LYSNR_SECRETS } from '@bytelyst/config'; +import { + collectDevopsInfo, + getBuildInfo, + httpDependencyCheck, + readServiceVersion, +} from '@bytelyst/devops/server'; await resolveSecrets([ LYSNR_SECRETS.COSMOS_KEY, LYSNR_SECRETS.COSMOS_ENDPOINT, @@ -281,6 +287,51 @@ await app.register(eventSubscriptionRoutes, { prefix: '/api' }); // Agent executor + tool registry + scheduling + metrics await app.register(agentExecutorRoutes, { prefix: '/api' }); +// DevOps endpoints +await app.register( + async function (fastify) { + // 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 (request, reply) => { + // Require admin role + const auth = (request as any).auth; + if (!auth || auth.role !== 'admin') { + return reply.code(403).send({ error: 'Admin access required' }); + } + }, + }, + async (request, reply) => { + try { + const config = (await import('@bytelyst/config')).loadProductIdentity(); + const info = await collectDevopsInfo({ + productId: config.productId || 'platform', + serviceName: 'platform-service', + serviceVersion: readServiceVersion(import.meta.url), + dependencyChecks: [ + () => httpDependencyCheck('cosmos-db', config.cosmosEndpoint || 'unknown'), + ], + extra: { + platformServiceUrl: config.platformServiceUrl, + }, + }); + 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 event bus subscribers registerDiagnosticsSubscribers(app.log); registerDeliverySubscribers(app.log);