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:
parent
35bf51302c
commit
c39da91588
@ -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:*",
|
||||
|
||||
97
dashboards/admin-web/src/app/devops/page.tsx
Normal file
97
dashboards/admin-web/src/app/devops/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@ -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:*",
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user