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>
This commit is contained in:
root 2026-05-11 03:37:59 +00:00
parent 35bf51302c
commit c39da91588
5 changed files with 152 additions and 0 deletions

View File

@ -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:*",

View File

@ -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<DevopsInfo> {
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<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 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 (
<div className="container mx-auto py-8 px-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

@ -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 },
];

View File

@ -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:*",

View File

@ -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);