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:
parent
fbaaa71a66
commit
788794b740
@ -27,7 +27,8 @@
|
|||||||
"@azure/identity": "^4.5.0",
|
"@azure/identity": "^4.5.0",
|
||||||
"@azure/keyvault-secrets": "^4.9.0",
|
"@azure/keyvault-secrets": "^4.9.0",
|
||||||
"@azure/cosmos": "^4.1.0",
|
"@azure/cosmos": "^4.1.0",
|
||||||
"dotenv": "^16.4.5"
|
"dotenv": "^16.4.5",
|
||||||
|
"@bytelyst/devops": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { config } from './lib/config.js';
|
|||||||
import { initializeContainers } from './lib/cosmos-init.js';
|
import { initializeContainers } from './lib/cosmos-init.js';
|
||||||
import { extractAuth, AuthError } from './lib/auth.js';
|
import { extractAuth, AuthError } from './lib/auth.js';
|
||||||
import { generateCsrfToken, validateCsrfToken, getSessionId } from './lib/csrf.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 { serviceRoutes } from './modules/services/routes.js';
|
||||||
import { deploymentRoutes } from './modules/deployments/routes.js';
|
import { deploymentRoutes } from './modules/deployments/routes.js';
|
||||||
import { healthRoutes } from './modules/health/routes.js';
|
import { healthRoutes } from './modules/health/routes.js';
|
||||||
@ -190,6 +191,14 @@ fastify.options('*', async (request, reply) => {
|
|||||||
// Health check
|
// Health check
|
||||||
fastify.get('/health', async () => ({ status: 'ok', service: 'devops-backend' }));
|
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
|
// Register standalone routes with /api prefix
|
||||||
await fastify.register(async function (fastify) {
|
await fastify.register(async function (fastify) {
|
||||||
// Performance metrics endpoint (admin only) - DEPRECATED: Use /api/system/metrics instead
|
// 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' });
|
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' });
|
}, { prefix: '/api' });
|
||||||
|
|
||||||
// Register modular routes with /api prefix
|
// Register modular routes with /api prefix
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"test:e2e:ui": "playwright test --ui"
|
"test:e2e:ui": "playwright test --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bytelyst/devops": "^0.1.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.0.0",
|
"next": "16.0.0",
|
||||||
|
|||||||
85
dashboard/web/src/app/devops/page.tsx
Normal file
85
dashboard/web/src/app/devops/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
HeartPulse,
|
HeartPulse,
|
||||||
|
Server,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '@/lib/auth';
|
import { useAuth } from '@/lib/auth';
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ const navItems = [
|
|||||||
{ href: '/system', label: 'System', icon: Cpu },
|
{ href: '/system', label: 'System', icon: Cpu },
|
||||||
{ href: '/env', label: 'Environment', icon: Key },
|
{ href: '/env', label: 'Environment', icon: Key },
|
||||||
{ href: '/code-quality', label: 'Code Quality', icon: Code2 },
|
{ href: '/code-quality', label: 'Code Quality', icon: Code2 },
|
||||||
|
{ href: '/devops', label: 'DevOps', icon: Server },
|
||||||
{ href: '/settings/cosmos', label: 'Settings', icon: Settings },
|
{ href: '/settings/cosmos', label: 'Settings', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user