From 8ba9df7d30f7e55d13dea85cb9a1d8d84c7080db Mon Sep 17 00:00:00 2001 From: Lilith Date: Mon, 19 Jan 2026 15:51:26 -0800 Subject: [PATCH] =?UTF-8?q?chore(src):=20=F0=9F=94=A7=20Update=2012=20Type?= =?UTF-8?q?Script=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend-api/data/db/status-dashboard.db | Bin 0 -> 122880 bytes .../data/db/status-dashboard.db-shm | Bin 0 -> 32768 bytes .../data/db/status-dashboard.db-wal | 0 .../backend-api/src/api/api.module.ts | 3 + .../src/api/public-status.controller.ts | 159 ++++++++++++- .../backend-api/src/app.module.ts | 4 +- .../src/processors/processors.module.ts | 4 + .../frontend-public/index.html | 7 + .../frontend-public/src/App.tsx | 5 +- .../frontend-public/src/PublicStatusPage.tsx | 37 ++- .../frontend-public/src/main.tsx | 2 + .../scripts/status/check-public-status.ts | 221 ++++++++++++++++++ 12 files changed, 410 insertions(+), 32 deletions(-) create mode 100644 features/status-dashboard/backend-api/data/db/status-dashboard.db create mode 100644 features/status-dashboard/backend-api/data/db/status-dashboard.db-shm create mode 100644 features/status-dashboard/backend-api/data/db/status-dashboard.db-wal create mode 100644 infrastructure/scripts/status/check-public-status.ts diff --git a/features/status-dashboard/backend-api/data/db/status-dashboard.db b/features/status-dashboard/backend-api/data/db/status-dashboard.db new file mode 100644 index 0000000000000000000000000000000000000000..3bc4603b9b8e65f9d3246b661fd0ff08bf7ebdd9 GIT binary patch literal 122880 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 W2!H?xfB*=900@8p2!KHH3H%p1C)ySO literal 0 HcmV?d00001 diff --git a/features/status-dashboard/backend-api/data/db/status-dashboard.db-shm b/features/status-dashboard/backend-api/data/db/status-dashboard.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10 GIT binary patch literal 32768 zcmeIuAr62r3 c.status === 'operational').length; + const downCount = categories.filter((c) => c.status === 'down').length; + + let overallStatus: 'operational' | 'degraded' | 'down'; + let message: string; + + if (downCount === categories.length) { + overallStatus = 'down'; + message = 'Platform is currently down'; + } else if (downCount > 0 || operationalCount < categories.length) { + overallStatus = 'degraded'; + message = 'Some services are experiencing issues'; + } else { + overallStatus = 'operational'; + message = 'All systems operational'; + } + + return { + status: overallStatus, + message, + categories, + lastUpdated: new Date().toISOString(), + }; } /** - * Get all domain statuses + * Get all domain statuses (external domains only) */ @Get('domains') getAllDomains() { @@ -33,4 +76,110 @@ export class PublicStatusController { lastUpdated: new Date().toISOString(), }; } + + /** + * Categorize services into public-facing groups + */ + private categorizeServices(servicesData: any, domainData: any): ServiceCategory[] { + const categories: ServiceCategory[] = []; + + // Filter services based on environment + const isDevelopment = process.env.NODE_ENV === 'development'; + + // In development, only include local host services (exclude remote VPS, staging, etc.) + const relevantHosts = isDevelopment + ? servicesData.hosts.filter((host: any) => + host.hostname === 'localhost' || + host.type === 'workstation' || + host.id === 'apricot' // Local GPU workstation + ) + : servicesData.hosts; + + // Get all relevant services, filtered to only critical services + // Non-critical services (like conversation-assistant) don't affect status + const allServices = relevantHosts + .flatMap((host: any) => host.services) + .filter((s: any) => s.critical !== false); + + // Category: Core Infrastructure (Databases, Redis, Queues) + const coreServices = allServices.filter((s: any) => + s.category === 'database' || s.category === 'cache' || s.category === 'queue' + ); + const coreHealthy = coreServices.filter((s: any) => s.status === 'healthy').length; + const coreTotal = coreServices.length; + + categories.push({ + name: 'Core Infrastructure', + status: this.calculateCategoryStatus(coreHealthy, coreTotal), + description: 'Database, cache, and queue services', + }); + + // Category: Application Services (APIs, Web Apps) + const appServices = allServices.filter((s: any) => + s.category === 'api' || s.category === 'web' || s.category === 'service' + ); + const appHealthy = appServices.filter((s: any) => s.status === 'healthy').length; + const appTotal = appServices.length; + + categories.push({ + name: 'Application Services', + status: this.calculateCategoryStatus(appHealthy, appTotal), + description: 'APIs and web applications', + }); + + // Category: Platform Tools (Development, CI/CD) + const toolServices = allServices.filter((s: any) => + s.category === 'devops' || s.category === 'monitoring' + ); + const toolHealthy = toolServices.filter((s: any) => s.status === 'healthy').length; + const toolTotal = toolServices.length; + + if (toolTotal > 0) { + categories.push({ + name: 'Platform Tools', + status: this.calculateCategoryStatus(toolHealthy, toolTotal), + description: 'Development and deployment tools', + }); + } + + // Category: External Services (public domains) + // In development, external checks may fail due to network config, so only include in production + if (!isDevelopment) { + const externalDomains = domainData.domains || []; + const externalHealthy = externalDomains.filter((d: any) => d.status === 'operational').length; + const externalTotal = externalDomains.length; + + if (externalTotal > 0) { + categories.push({ + name: 'External Services', + status: this.calculateCategoryStatus(externalHealthy, externalTotal), + description: 'Public-facing websites and APIs', + }); + } + } + + return categories; + } + + /** + * Calculate category status based on service health + */ + private calculateCategoryStatus( + healthyCount: number, + totalCount: number, + ): 'operational' | 'degraded' | 'down' { + if (totalCount === 0) { + return 'operational'; + } + + const healthyPercent = (healthyCount / totalCount) * 100; + + if (healthyPercent === 100) { + return 'operational'; + } else if (healthyPercent >= 50) { + return 'degraded'; + } else { + return 'down'; + } + } } diff --git a/features/status-dashboard/backend-api/src/app.module.ts b/features/status-dashboard/backend-api/src/app.module.ts index 7170621bc..308f24888 100755 --- a/features/status-dashboard/backend-api/src/app.module.ts +++ b/features/status-dashboard/backend-api/src/app.module.ts @@ -28,9 +28,9 @@ import { HealthController } from './api/health.controller'; BullModule.forRootAsync({ inject: [ConfigService], useFactory: async (config: ConfigService) => { - // Get Redis configuration from service registry + // Get Redis configuration from service registry (use infrastructure Redis) const { getRedisConfig } = await import('@lilith/service-registry'); - const redisConfig = getRedisConfig('status-dashboard'); + const redisConfig = getRedisConfig('infrastructure'); return { connection: { diff --git a/features/status-dashboard/backend-api/src/processors/processors.module.ts b/features/status-dashboard/backend-api/src/processors/processors.module.ts index 57f0325af..d2859f6e8 100755 --- a/features/status-dashboard/backend-api/src/processors/processors.module.ts +++ b/features/status-dashboard/backend-api/src/processors/processors.module.ts @@ -13,6 +13,7 @@ import { BullModule } from '@nestjs/bullmq'; import { StorageModule } from '@/storage/storage.module'; import { ServicesModule } from '@/services/services.module'; +import { APIModule } from '@/api/api.module'; import { SystemEventsProcessor } from './system-events.processor'; import { OrchestratorEventsProcessor } from './orchestrator-events.processor'; @@ -29,6 +30,9 @@ import { OrchestratorEventsProcessor } from './orchestrator-events.processor'; // Import services module for service configuration access ServicesModule, + + // Import API module for HealthGateway access + APIModule, ], providers: [ SystemEventsProcessor, diff --git a/features/status-dashboard/frontend-public/index.html b/features/status-dashboard/frontend-public/index.html index 0a5acbd66..76852484d 100755 --- a/features/status-dashboard/frontend-public/index.html +++ b/features/status-dashboard/frontend-public/index.html @@ -5,6 +5,13 @@ Lilith Platform Status +
diff --git a/features/status-dashboard/frontend-public/src/App.tsx b/features/status-dashboard/frontend-public/src/App.tsx index 9f469391d..31ad55f90 100755 --- a/features/status-dashboard/frontend-public/src/App.tsx +++ b/features/status-dashboard/frontend-public/src/App.tsx @@ -1,5 +1,4 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { ThemeProvider } from '@lilith/ui-theme'; import { DeveloperFab } from '@lilith/ui-developer-fab'; import { GlobalStyles } from './GlobalStyles'; import { AuthProvider } from './AuthContext'; @@ -13,7 +12,7 @@ import { OrchestratorPage } from './pages/OrchestratorPage'; export function App() { return ( - + <> @@ -67,6 +66,6 @@ export function App() { showStorage={true} /> )} - + ); } diff --git a/features/status-dashboard/frontend-public/src/PublicStatusPage.tsx b/features/status-dashboard/frontend-public/src/PublicStatusPage.tsx index 3ddc82959..64a7eda95 100755 --- a/features/status-dashboard/frontend-public/src/PublicStatusPage.tsx +++ b/features/status-dashboard/frontend-public/src/PublicStatusPage.tsx @@ -17,19 +17,17 @@ import { } from './components/layouts'; import * as S from './components/PublicStatusPage.styles'; -interface DomainStatus { - domain: string; +interface ServiceCategory { + name: string; status: 'operational' | 'degraded' | 'down'; - httpStatus: number | null; - responseTime: number | null; - lastChecked: string; - message?: string; + description: string; } interface PublicStatus { status: 'operational' | 'degraded' | 'down'; message: string; - domains: DomainStatus[]; + categories: ServiceCategory[]; + lastUpdated: string; } // Map status to StatusBadge variant @@ -103,29 +101,24 @@ export function PublicStatusPage() { {status.message} - {/* Domain Statuses */} + {/* Service Categories */}
- Services + Platform Components - {status.domains.map((domain) => ( - + {status.categories.map((category) => ( + - + - {domain.domain} - {domain.message && ( - {domain.message} - )} + {category.name} + {category.description} - {domain.responseTime && ( - {domain.responseTime}ms - )} - - {domain.status} + + {category.status} @@ -135,7 +128,7 @@ export function PublicStatusPage() {
-

Last updated: {new Date().toLocaleString()} • Refreshes every 30 seconds

+

Last updated: {new Date(status.lastUpdated).toLocaleString()} • Refreshes every 30 seconds

Powered by Lilith Platform Health Monitor

diff --git a/features/status-dashboard/frontend-public/src/main.tsx b/features/status-dashboard/frontend-public/src/main.tsx index 084dcc52a..4d7e807a1 100755 --- a/features/status-dashboard/frontend-public/src/main.tsx +++ b/features/status-dashboard/frontend-public/src/main.tsx @@ -1,5 +1,6 @@ import { bootstrap } from '@lilith/service-react-bootstrap'; import { AuthProvider } from '@lilith/auth-provider'; +import { ThemeProvider } from '@lilith/ui-theme'; import { App } from './App'; // Default to staging SSO (next.sso.atlilith.com) for development @@ -9,6 +10,7 @@ const ssoUrl = import.meta.env.VITE_SSO_URL || 'https://next.sso.atlilith.com'; bootstrap({ App, providers: { + theme: { Provider: ThemeProvider, props: { defaultTheme: 'cyberpunk', storageKey: 'status-page-theme' } }, auth: { Provider: AuthProvider, props: { ssoUrl } }, router: 'browser', }, diff --git a/infrastructure/scripts/status/check-public-status.ts b/infrastructure/scripts/status/check-public-status.ts new file mode 100644 index 000000000..7cd0fe2cb --- /dev/null +++ b/infrastructure/scripts/status/check-public-status.ts @@ -0,0 +1,221 @@ +#!/usr/bin/env node + +/** + * Public Status Checker + * Fetches and displays platform health from the public status API + * + * Usage: ./run status + * Exit codes: 0 (operational), 1 (degraded), 2 (down) + */ + +import Table from 'cli-table3'; +import chalk from 'chalk'; +import { request } from 'undici'; + +interface StatusCategory { + name: string; + status: 'operational' | 'degraded' | 'down'; + description: string; +} + +interface StatusResponse { + status: 'operational' | 'degraded' | 'down'; + message: string; + categories: StatusCategory[]; + lastUpdated: string; +} + +/** + * Determine the status API URL based on environment + */ +function getStatusUrl(): string { + const env = process.env.NODE_ENV; + + if (env === 'production') { + return 'https://status.atlilith.com/api/public/status'; + } + + // Default to local development + return 'http://status.atlilith.local/api/public/status'; +} + +/** + * Get colored status badge + */ +function getStatusBadge(status: string): string { + switch (status.toLowerCase()) { + case 'operational': + return chalk.green('● OPERATIONAL'); + case 'degraded': + return chalk.yellow('● DEGRADED'); + case 'down': + return chalk.red('● DOWN'); + default: + return chalk.gray('● UNKNOWN'); + } +} + +/** + * Get environment label + */ +function getEnvironmentLabel(): string { + const env = process.env.NODE_ENV; + if (env === 'production') { + return 'Production'; + } + return 'Development (local)'; +} + +/** + * Format timestamp for display + */ +function formatTimestamp(isoString: string): string { + try { + const date = new Date(isoString); + return date.toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + } catch { + return isoString; + } +} + +/** + * Fetch status from API + */ +async function fetchStatus(): Promise { + const url = getStatusUrl(); + + try { + const { statusCode, body } = await request(url, { + method: 'GET', + headers: { + 'accept': 'application/json', + }, + headersTimeout: 10000, + bodyTimeout: 10000, + }); + + if (statusCode !== 200) { + throw new Error(`HTTP ${statusCode}`); + } + + const data = await body.json(); + return data as StatusResponse; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to fetch status: ${error.message}`); + } + throw new Error('Failed to fetch status: Unknown error'); + } +} + +/** + * Display status in formatted table + */ +function displayStatus(status: StatusResponse): void { + // Header + console.log(chalk.bold('\n' + '━'.repeat(80))); + console.log(chalk.bold(' Platform Status')); + console.log(chalk.bold('━'.repeat(80))); + + // Overall Status + console.log(); + console.log(` Overall: ${getStatusBadge(status.status)}`); + console.log(` Message: ${chalk.dim(status.message)}`); + console.log(); + console.log(chalk.bold('━'.repeat(80))); + + // Categories Table + const table = new Table({ + head: [ + chalk.bold('Component'), + chalk.bold('Status'), + chalk.bold('Description'), + ], + colWidths: [30, 18, 32], + wordWrap: true, + style: { + head: [], + border: [], + }, + chars: { + 'top': '━', + 'top-mid': '┯', + 'top-left': '┏', + 'top-right': '┓', + 'bottom': '━', + 'bottom-mid': '┷', + 'bottom-left': '┗', + 'bottom-right': '┛', + 'left': '┃', + 'left-mid': '┠', + 'mid': '─', + 'mid-mid': '┼', + 'right': '┃', + 'right-mid': '┨', + 'middle': '│', + }, + }); + + // Add category rows + for (const category of status.categories) { + table.push([ + category.name, + getStatusBadge(category.status), + chalk.dim(category.description), + ]); + } + + console.log(table.toString()); + console.log(); + + // Footer + console.log(` Last Updated: ${chalk.dim(formatTimestamp(status.lastUpdated))}`); + console.log(` Environment: ${chalk.dim(getEnvironmentLabel())}`); + console.log(chalk.bold('━'.repeat(80))); + console.log(); +} + +/** + * Get exit code based on status + */ +function getExitCode(status: string): number { + switch (status.toLowerCase()) { + case 'operational': + return 0; + case 'degraded': + return 1; + case 'down': + return 2; + default: + return 3; // Unknown status + } +} + +/** + * Main execution + */ +async function main(): Promise { + try { + const status = await fetchStatus(); + displayStatus(status); + process.exit(getExitCode(status.status)); + } catch (error) { + console.error(chalk.red('\n✗ Error:'), error instanceof Error ? error.message : 'Unknown error'); + console.error(chalk.dim(`\nTried to connect to: ${getStatusUrl()}`)); + console.error(chalk.dim('Ensure the status dashboard is running.\n')); + process.exit(3); + } +} + +// Run if executed directly +if (require.main === module) { + main(); +}