diff --git a/backend/package.json b/backend/package.json index c8070a3..7a74f77 100644 --- a/backend/package.json +++ b/backend/package.json @@ -77,7 +77,7 @@ "socket.io": "^4.8.3", "winston": "^3.19.0", "@bytelyst/telemetry-client": "*", - "@bytelyst/devops": "^0.1.1" + "@bytelyst/devops": "^0.1.2" }, "devDependencies": { "@types/node": "^25.0.3", diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index 9dc99c6..ec15b29 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -3,7 +3,8 @@ import { createServer } from 'http'; import { Server, Socket } from 'socket.io'; import cors from 'cors'; import { randomUUID } from 'node:crypto'; -import { collectDevopsInfo } from '@bytelyst/devops/server'; +import { collectDevopsInfo, dependencyCheck, getBuildInfo, httpDependencyCheck, readServiceVersion } from '@bytelyst/devops/server'; +import { getContainer } from '@bytelyst/cosmos'; import logger from '../utils/logger.js'; import fs from 'fs'; import path from 'path'; @@ -2365,13 +2366,31 @@ export class ApiServer { res.json(flags); }); - // ── DevOps info: build, runtime, config (auth required) ────────────── - this.app.get('/api/devops/info', this.requireAuth, async (_req, res) => { + // ── DevOps version: PUBLIC, no auth (build SHA + branch + image) ───── + // Useful for ops/CI health checks and rollback verification. + this.app.get('/api/devops/version', (_req, res) => { + res.json(getBuildInfo()); + }); + + // ── DevOps info: full payload (admin-only — exposes env keys, deps) ── + this.app.get('/api/devops/info', this.requireAuth, this.requireAdmin, async (_req, res) => { try { const info = await collectDevopsInfo({ productId: config.PRODUCT_ID || 'invttrdg', serviceName: 'trading-backend', - serviceVersion: process.env.npm_package_version || '0.1.0', + serviceVersion: readServiceVersion(import.meta.url), + dependencyChecks: [ + () => dependencyCheck('cosmos', async () => { + // Lightweight ping: read database properties. + const c = getContainer('trading_users'); + await c.items.query('SELECT TOP 1 c.id FROM c').fetchNext(); + }), + () => httpDependencyCheck('platform-service', `${config.PLATFORM_API_URL}/health`), + ], + extra: { + socketIoConnections: this.io?.engine?.clientsCount ?? 0, + tradingApiUrl: config.PLATFORM_API_URL, + }, }); res.json(info); } catch (error: any) { diff --git a/backend/src/services/profileRepository.ts b/backend/src/services/profileRepository.ts index a64ef4c..22f9251 100644 --- a/backend/src/services/profileRepository.ts +++ b/backend/src/services/profileRepository.ts @@ -719,7 +719,10 @@ export async function saveCurrentUserProfile( ...input, user_id: userId, email: String(input.email ?? existing.email ?? fallback.email ?? ''), - role: String(input.role ?? existing.role ?? fallback.role ?? 'member'), + // Role is intentionally NOT persisted from client input — JWT role is the + // authoritative source. We keep the existing stored role for backward + // compatibility but it's overridden on read at the API layer. + role: String(existing.role ?? fallback.role ?? 'member'), first_name: String(input.first_name ?? existing.first_name ?? fallback.first_name ?? ''), last_name: String(input.last_name ?? existing.last_name ?? fallback.last_name ?? ''), trade_enable: Boolean(input.trade_enable ?? existing.trade_enable ?? fallback.trade_enable ?? true), diff --git a/web/package.json b/web/package.json index 50ec37b..8a090ec 100644 --- a/web/package.json +++ b/web/package.json @@ -42,7 +42,7 @@ "react-router-dom": "^7.14.2", "recharts": "^3.6.0", "socket.io-client": "^4.8.3", - "@bytelyst/devops": "^0.1.1" + "@bytelyst/devops": "^0.1.2" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/web/src/tabs/DevOpsTab.tsx b/web/src/tabs/DevOpsTab.tsx index 5b97157..e53a99c 100644 --- a/web/src/tabs/DevOpsTab.tsx +++ b/web/src/tabs/DevOpsTab.tsx @@ -8,7 +8,9 @@ import { DevopsPanel, type DevopsInfo } from '@bytelyst/devops/ui'; import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; -const VERSION = '0.1.0'; +// Injected by Vite at build time (see web/vite.config.ts → define). +declare const __WEB_SERVICE_VERSION__: string; +const VERSION = typeof __WEB_SERVICE_VERSION__ !== 'undefined' ? __WEB_SERVICE_VERSION__ : '0.0.0'; async function fetchBackendInfo(): Promise { const token = await getPlatformAccessToken(); diff --git a/web/src/views/SettingsView.tsx b/web/src/views/SettingsView.tsx index 74fd721..9a4c518 100644 --- a/web/src/views/SettingsView.tsx +++ b/web/src/views/SettingsView.tsx @@ -17,13 +17,13 @@ export function SettingsView() { 'Account', ...(isAdmin ? ['Bot Config' as SettingsSection] : []), ...(isAdmin ? ['Admin Panel' as SettingsSection] : []), - 'DevOps', + ...(isAdmin ? ['DevOps' as SettingsSection] : []), ]; const requestedSection = searchParams.get('section'); const initialSection = useMemo(() => { if (requestedSection === 'Bot Config' && isAdmin) return 'Bot Config'; if (requestedSection === 'Admin Panel' && isAdmin) return 'Admin Panel'; - if (requestedSection === 'DevOps') return 'DevOps'; + if (requestedSection === 'DevOps' && isAdmin) return 'DevOps'; return 'Account'; }, [requestedSection, isAdmin]); const [section, setSection] = useState(initialSection); @@ -64,7 +64,7 @@ export function SettingsView() { {section === 'Account' && } {section === 'Bot Config' && isAdmin && } {section === 'Admin Panel' && isAdmin && } - {section === 'DevOps' && } + {section === 'DevOps' && isAdmin && } ); diff --git a/web/vite.config.ts b/web/vite.config.ts index 02f0da7..522adcf 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -9,6 +9,10 @@ import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; import { playwright } from '@vitest/browser-playwright'; const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); +// Read service version from package.json so DevOps panel reflects the real version. +const webPackageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf8')) as { version?: string }; +const webServiceVersion = webPackageJson.version || '0.0.0'; + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon const monacoEditorPath = path.resolve(__dirname, 'node_modules/monaco-editor'); const workspaceRoot = path.resolve(__dirname, '..', '..'); @@ -32,6 +36,9 @@ function bytelystAlias(pkg: string): string { // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + define: { + __WEB_SERVICE_VERSION__: JSON.stringify(webServiceVersion), + }, // Shared files (../shared/*.ts) live outside web/ so Vite resolves their imports // from the repo root where @bytelyst/* are not installed. Redirect all @bytelyst/* // imports first to web/node_modules, then fall back to the monorepo vendor/ dir.