diff --git a/features/age-verification/services.yaml b/features/age-verification/services.yaml index 3bfaca9c6..dd2ca9b7f 100644 --- a/features/age-verification/services.yaml +++ b/features/age-verification/services.yaml @@ -22,7 +22,7 @@ services: type: process name: age-verification dependencies: - - platform.sso + - sso.api - infrastructure.postgresql deployments: diff --git a/features/marketplace/services.yaml b/features/marketplace/services.yaml index 1d47a73c8..eca3f7685 100644 --- a/features/marketplace/services.yaml +++ b/features/marketplace/services.yaml @@ -26,7 +26,7 @@ services: - infrastructure.postgresql - marketplace.postgresql - infrastructure.redis - - platform.sso + - sso.api - id: postgresql name: Marketplace Database @@ -42,7 +42,7 @@ services: description: Vite dev server dependencies: - marketplace.api - - platform.sso + - sso.api - seo.api deployments: diff --git a/features/merchant/backend-api/Dockerfile.e2e b/features/merchant/backend-api/Dockerfile.e2e index a8c80dd3e..fdf01a0a1 100644 --- a/features/merchant/backend-api/Dockerfile.e2e +++ b/features/merchant/backend-api/Dockerfile.e2e @@ -53,9 +53,9 @@ COPY features/merchant/backend-api/package.json ./features/merchant/backend-api/ # Copy workspace packages COPY @packages ./@packages -# Install production dependencies only +# Install all dependencies (including dev dependencies for testing) # Use --ignore-scripts to skip workspace package prepare scripts that need full context -RUN pnpm install --frozen-lockfile --prod --filter @lilith/merchant-api --ignore-scripts || pnpm install --filter @lilith/merchant-api --ignore-scripts +RUN pnpm install --frozen-lockfile --filter @lilith/merchant-api --ignore-scripts || pnpm install --filter @lilith/merchant-api --ignore-scripts # Copy built application from builder COPY --from=builder /app/features/merchant/backend-api/dist ./features/merchant/backend-api/dist @@ -65,6 +65,10 @@ COPY --from=builder /app/features/merchant/backend-api/test ./features/merchant/ COPY --from=builder /app/features/merchant/backend-api/jest.config.js ./features/merchant/backend-api/ COPY --from=builder /app/features/merchant/backend-api/tsconfig.json ./features/merchant/backend-api/ +# Copy E2E infrastructure config (required by @lilith/service-addresses) +# The package looks for /infrastructure/ports.yaml and /infrastructure/services/features/*.yaml +COPY features/merchant/backend-api/test/fixtures/infrastructure/ /infrastructure/ + # Set working directory to merchant backend-api WORKDIR /app/features/merchant/backend-api diff --git a/features/merchant/backend-api/src/app.module.ts b/features/merchant/backend-api/src/app.module.ts index 5ee4a21e0..c72d26b40 100644 --- a/features/merchant/backend-api/src/app.module.ts +++ b/features/merchant/backend-api/src/app.module.ts @@ -53,6 +53,11 @@ import { SubscriptionsModule } from './subscriptions/subscriptions.module' database: config.get('DATABASE_POSTGRES_NAME'), }) + // Allow explicit synchronize override for E2E tests (where seed.sql provides schema) + const synchronize = config.get('DATABASE_SYNCHRONIZE') === 'false' + ? false + : config.get('NODE_ENV') !== 'production' + return { type: 'postgres', host: dbConfig.host, @@ -61,7 +66,7 @@ import { SubscriptionsModule } from './subscriptions/subscriptions.module' password: dbConfig.password, database: dbConfig.database, autoLoadEntities: true, - synchronize: config.get('NODE_ENV') !== 'production', + synchronize, logging: config.get('NODE_ENV') !== 'production', } }, diff --git a/features/merchant/backend-api/test/fixtures/infrastructure/ports.yaml b/features/merchant/backend-api/test/fixtures/infrastructure/ports.yaml new file mode 100644 index 000000000..ab8fef553 --- /dev/null +++ b/features/merchant/backend-api/test/fixtures/infrastructure/ports.yaml @@ -0,0 +1,12 @@ +# E2E Test Ports Configuration +# Minimal subset of infrastructure/ports.yaml for E2E testing + +infrastructure: + postgresql: 5432 + redis: 6379 + +features: + merchant: + api: 3020 + postgresql: 5432 # E2E uses shared postgres container + redis: 6379 # E2E uses shared redis container diff --git a/features/merchant/backend-api/test/fixtures/infrastructure/services/features/merchant.yaml b/features/merchant/backend-api/test/fixtures/infrastructure/services/features/merchant.yaml new file mode 100644 index 000000000..4732dc0dc --- /dev/null +++ b/features/merchant/backend-api/test/fixtures/infrastructure/services/features/merchant.yaml @@ -0,0 +1,40 @@ +# ============================================================================= +# Merchant (E2E Test Config) +# ============================================================================= +# Minimal service configuration for E2E Docker testing + +feature: + id: merchant + name: Merchant + description: Centralized product catalog, subscription tiers, and merchant services + owner: platform-core + +ports: + api: 3020 + postgresql: 5432 # E2E uses shared postgres container (not dedicated port) + redis: 6379 # E2E uses shared redis container (not dedicated port) + +services: + - id: api + name: Merchant API + type: backend + port: 3020 + entrypoint: codebase/features/merchant/backend-api + description: Product catalog, subscription tiers, inventory management + health_check: /health + dependencies: + - infrastructure.postgresql + - merchant.postgresql + - infrastructure.redis + + - id: postgresql + name: Merchant Database + type: postgresql + port: 5432 # E2E uses shared postgres container + description: Products, variants, subscription tiers, orders + + - id: redis + name: Merchant Cache + type: redis + port: 6379 # E2E uses shared redis container + description: Product cache, inventory locks diff --git a/features/payments/services.yaml b/features/payments/services.yaml index 41724d996..312170586 100644 --- a/features/payments/services.yaml +++ b/features/payments/services.yaml @@ -28,7 +28,7 @@ services: - infrastructure.postgresql - payments.postgresql - payments.redis - - platform.sso + - sso.api - id: postgresql name: Payments Database diff --git a/features/platform-admin/backend-api/src/infrastructure/infrastructure.service.ts b/features/platform-admin/backend-api/src/infrastructure/infrastructure.service.ts index 67e570fb3..9d1c30de9 100644 --- a/features/platform-admin/backend-api/src/infrastructure/infrastructure.service.ts +++ b/features/platform-admin/backend-api/src/infrastructure/infrastructure.service.ts @@ -275,6 +275,7 @@ export class InfrastructureService { gpu: service.gpu, critical: service.critical, dependencies: service.dependencies || [], + optionalDependencies: service.optionalDependencies || [], }; } diff --git a/features/platform-admin/backend-api/src/infrastructure/types/feature-group.dto.ts b/features/platform-admin/backend-api/src/infrastructure/types/feature-group.dto.ts index f03a7200e..5a5dd5faa 100644 --- a/features/platform-admin/backend-api/src/infrastructure/types/feature-group.dto.ts +++ b/features/platform-admin/backend-api/src/infrastructure/types/feature-group.dto.ts @@ -42,6 +42,13 @@ export class ServiceNodeDto { example: ['infrastructure.postgresql', 'analytics.redis'], }) dependencies: string[]; + + @ApiProperty({ + type: [String], + description: 'Optional dependencies (service can start without these)', + example: ['marketplace.api', 'conversation-assistant.api'], + }) + optionalDependencies: string[]; } /** diff --git a/features/platform-admin/frontend-admin/package.json b/features/platform-admin/frontend-admin/package.json index 4fe0fbf9a..571bca39b 100644 --- a/features/platform-admin/frontend-admin/package.json +++ b/features/platform-admin/frontend-admin/package.json @@ -33,7 +33,7 @@ "@lilith/ui-admin": "^1.0.0", "@lilith/ui-charts": "^1.0.0", "@lilith/ui-data": "^1.0.0", - "@lilith/ui-dev-tools": "^1.1.5", + "@lilith/ui-dev-tools": "^1.1.4", "@lilith/ui-error-pages": "^1.1.3", "@lilith/ui-fab": "^2.0.1", "@lilith/ui-feedback": "^1.1.1", diff --git a/features/platform-admin/frontend-admin/src/components/queue/QueueStatusCard.test.tsx b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusCard.test.tsx new file mode 100644 index 000000000..cd2840f2c --- /dev/null +++ b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusCard.test.tsx @@ -0,0 +1,133 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ThemeProvider } from 'styled-components'; +import { cyberpunkAdapter } from '@lilith/ui-theme'; +import { QueueStatusCard } from './QueueStatusCard'; +import type { QueueStats, QueueDetails } from './types'; + +const mockQueue: QueueStats = { + name: 'test-queue', + waiting: 5, + active: 2, + completed: 1000, + failed: 3, +}; + +const mockDetails: QueueDetails = { + ...mockQueue, + avgProcessingTime: 234.5, + throughput: 12.5, + lastProcessedAt: '2026-01-10T12:00:00Z', +}; + +const renderWithTheme = (component: React.ReactElement) => { + return render({component}); +}; + +describe('QueueStatusCard', () => { + describe('rendering', () => { + it('should render queue name', () => { + renderWithTheme(); + expect(screen.getByTestId('queue-name')).toHaveTextContent('test-queue'); + }); + + it('should render status as Active when queue has work', () => { + renderWithTheme(); + expect(screen.getByTestId('queue-status')).toHaveTextContent('Active'); + }); + + it('should render status as Idle when queue has no work', () => { + const idleQueue = { ...mockQueue, waiting: 0, active: 0 }; + renderWithTheme(); + expect(screen.getByTestId('queue-status')).toHaveTextContent('Idle'); + }); + + it('should render all metrics', () => { + renderWithTheme(); + expect(screen.getByTestId('metric-waiting')).toHaveTextContent('5'); + expect(screen.getByTestId('metric-active')).toHaveTextContent('2'); + expect(screen.getByTestId('metric-completed')).toHaveTextContent('1000'); + expect(screen.getByTestId('metric-failed')).toHaveTextContent('3'); + }); + }); + + describe('details display', () => { + it('should render details when provided', () => { + renderWithTheme(); + const details = screen.getByTestId('queue-details'); + expect(details).toHaveTextContent('Avg: 235ms'); + expect(details).toHaveTextContent('Throughput: 12.5/min'); + expect(details).toHaveTextContent('Last:'); + }); + + it('should not render details section when not provided', () => { + renderWithTheme(); + expect(screen.queryByTestId('queue-details')).not.toBeInTheDocument(); + }); + }); + + describe('number formatting', () => { + it('should format numbers < 1000 as-is', () => { + const smallQueue = { ...mockQueue, completed: 999 }; + renderWithTheme(); + expect(screen.getByTestId('metric-completed')).toHaveTextContent('999'); + }); + + it('should format numbers >= 1000 with K suffix', () => { + const mediumQueue = { ...mockQueue, completed: 5500 }; + renderWithTheme(); + expect(screen.getByTestId('metric-completed')).toHaveTextContent('5.5K'); + }); + + it('should format numbers >= 1000000 with M suffix', () => { + const largeQueue = { ...mockQueue, completed: 2500000 }; + renderWithTheme(); + expect(screen.getByTestId('metric-completed')).toHaveTextContent('2.5M'); + }); + }); + + describe('work detection', () => { + it('should detect work when waiting > 0', () => { + const queueWithWaiting = { ...mockQueue, waiting: 1, active: 0 }; + renderWithTheme(); + expect(screen.getByTestId('queue-status')).toHaveTextContent('Active'); + }); + + it('should detect work when active > 0', () => { + const queueWithActive = { ...mockQueue, waiting: 0, active: 1 }; + renderWithTheme(); + expect(screen.getByTestId('queue-status')).toHaveTextContent('Active'); + }); + + it('should be idle when both waiting and active = 0', () => { + const idleQueue = { ...mockQueue, waiting: 0, active: 0 }; + renderWithTheme(); + expect(screen.getByTestId('queue-status')).toHaveTextContent('Idle'); + }); + }); + + describe('edge cases', () => { + it('should handle zero values', () => { + const zeroQueue: QueueStats = { + name: 'zero-queue', + waiting: 0, + active: 0, + completed: 0, + failed: 0, + }; + renderWithTheme(); + expect(screen.getByTestId('metric-waiting')).toHaveTextContent('0'); + expect(screen.getByTestId('metric-active')).toHaveTextContent('0'); + expect(screen.getByTestId('metric-completed')).toHaveTextContent('0'); + expect(screen.getByTestId('metric-failed')).toHaveTextContent('0'); + }); + + it('should accept custom className', () => { + const { container } = renderWithTheme( + + ); + const card = container.querySelector('[data-testid="queue-status-card"]'); + expect(card).toHaveClass('custom-class'); + }); + }); +}); diff --git a/features/platform-admin/frontend-admin/src/components/queue/QueueStatusCard.tsx b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusCard.tsx new file mode 100644 index 000000000..05158dbe6 --- /dev/null +++ b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusCard.tsx @@ -0,0 +1,170 @@ +/** + * Detailed queue status card for queue monitoring pages. + * Displays comprehensive queue metrics including performance data. + */ + +import styled from 'styled-components'; +import { Card } from '@lilith/ui-primitives'; +import type { QueueStats, QueueDetails } from './types'; +import { hasQueueWork } from './types'; + +export interface QueueStatusCardProps { + /** Queue statistics */ + queue: QueueStats; + /** Optional detailed performance metrics */ + details?: QueueDetails; + /** Optional className for styling */ + className?: string; +} + +const StyledCard = styled(Card)<{ $hasWork: boolean }>` + padding: ${({ theme }) => theme.spacing.lg}; + border-left: 4px solid + ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; +`; + +const Header = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: ${({ theme }) => theme.spacing.md}; +`; + +const Name = styled.h3` + font-size: ${({ theme }) => theme.typography.fontSize.lg}; + font-weight: ${({ theme }) => theme.typography.fontWeight.semibold}; + color: ${({ theme }) => theme.colors.text.primary}; + margin: 0; +`; + +const Status = styled.div<{ $hasWork: boolean }>` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing.xs}; + font-size: ${({ theme }) => theme.typography.fontSize.sm}; + color: ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; +`; + +const StatusDot = styled.div<{ $hasWork: boolean }>` + width: 8px; + height: 8px; + border-radius: 50%; + background: ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; +`; + +const Metrics = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: ${({ theme }) => theme.spacing.md}; +`; + +const MetricItem = styled.div` + text-align: center; +`; + +const MetricValue = styled.div<{ $warning?: boolean }>` + font-size: ${({ theme }) => theme.typography.fontSize.xl}; + font-weight: ${({ theme }) => theme.typography.fontWeight.bold}; + color: ${({ theme, $warning }) => ($warning ? theme.colors.warning : theme.colors.text.primary)}; +`; + +const MetricLabel = styled.div` + font-size: ${({ theme }) => theme.typography.fontSize.xs}; + color: ${({ theme }) => theme.colors.text.muted}; + margin-top: ${({ theme }) => theme.spacing.xs}; +`; + +const DetailsRow = styled.div` + margin-top: ${({ theme }) => theme.spacing.md}; + padding-top: ${({ theme }) => theme.spacing.md}; + border-top: 1px solid ${({ theme }) => theme.colors.border}; + display: flex; + align-items: center; + justify-content: space-between; + font-size: ${({ theme }) => theme.typography.fontSize.sm}; + color: ${({ theme }) => theme.colors.text.muted}; +`; + +function formatNumber(n: number): string { + if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; + if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; + return n.toString(); +} + +function formatTimeAgo(dateStr?: string): string { + if (!dateStr) return 'Never'; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `${diffHours}h ago`; + const diffDays = Math.floor(diffHours / 24); + return `${diffDays}d ago`; +} + +/** + * Detailed queue status card with metrics. + * + * @example + * ```tsx + * + * ``` + */ +export function QueueStatusCard({ queue, details, className }: QueueStatusCardProps) { + const hasWork = hasQueueWork(queue); + + return ( + +
+ {queue.name} + + + {hasWork ? 'Active' : 'Idle'} + +
+ + + + 0} data-testid="metric-waiting"> + {formatNumber(queue.waiting)} + + Waiting + + + + {formatNumber(queue.active)} + Active + + + + + {formatNumber(queue.completed)} + + Completed + + + + 0} data-testid="metric-failed"> + {formatNumber(queue.failed)} + + Failed + + + + {details && ( + + Avg: {details.avgProcessingTime.toFixed(0)}ms + Throughput: {details.throughput.toFixed(1)}/min + Last: {formatTimeAgo(details.lastProcessedAt)} + + )} +
+ ); +} diff --git a/features/platform-admin/frontend-admin/src/components/queue/QueueStatusIndicator.test.tsx b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusIndicator.test.tsx new file mode 100644 index 000000000..f5bb2303f --- /dev/null +++ b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusIndicator.test.tsx @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ThemeProvider } from 'styled-components'; +import { cyberpunkAdapter } from '@lilith/ui-theme'; +import { QueueStatusIndicator } from './QueueStatusIndicator'; +import type { QueueStats } from './types'; + +const mockQueue: QueueStats = { + name: 'test-queue', + waiting: 5, + active: 2, + completed: 100, + failed: 3, +}; + +const renderWithTheme = (component: React.ReactElement) => { + return render({component}); +}; + +describe('QueueStatusIndicator', () => { + describe('rendering', () => { + it('should render queue name', () => { + renderWithTheme(); + expect(screen.getByTestId('queue-name')).toHaveTextContent('test-queue'); + }); + + it('should render waiting and active counts when queue has work', () => { + renderWithTheme(); + expect(screen.getByTestId('queue-count')).toHaveTextContent('5 waiting / 2 active'); + }); + + it('should render em dash when queue is idle', () => { + const idleQueue = { ...mockQueue, waiting: 0, active: 0 }; + renderWithTheme(); + expect(screen.getByTestId('queue-count')).toHaveTextContent('—'); + }); + }); + + describe('work detection', () => { + it('should detect work when waiting > 0', () => { + const queueWithWaiting = { ...mockQueue, waiting: 1, active: 0 }; + const { container } = renderWithTheme(); + const indicator = container.firstChild as HTMLElement; + expect(indicator).toHaveAttribute('data-testid', 'queue-status-indicator'); + }); + + it('should detect work when active > 0', () => { + const queueWithActive = { ...mockQueue, waiting: 0, active: 1 }; + const { container } = renderWithTheme(); + const indicator = container.firstChild as HTMLElement; + expect(indicator).toHaveAttribute('data-testid', 'queue-status-indicator'); + }); + + it('should detect work when both waiting and active > 0', () => { + const { container } = renderWithTheme(); + const indicator = container.firstChild as HTMLElement; + expect(indicator).toHaveAttribute('data-testid', 'queue-status-indicator'); + }); + + it('should be idle when both waiting and active = 0', () => { + const idleQueue = { ...mockQueue, waiting: 0, active: 0 }; + renderWithTheme(); + expect(screen.getByTestId('queue-count')).toHaveTextContent('—'); + }); + }); + + describe('edge cases', () => { + it('should handle zero values', () => { + const zeroQueue = { + name: 'zero-queue', + waiting: 0, + active: 0, + completed: 0, + failed: 0, + }; + renderWithTheme(); + expect(screen.getByTestId('queue-name')).toHaveTextContent('zero-queue'); + expect(screen.getByTestId('queue-count')).toHaveTextContent('—'); + }); + + it('should handle large numbers', () => { + const largeQueue = { ...mockQueue, waiting: 9999, active: 8888 }; + renderWithTheme(); + expect(screen.getByTestId('queue-count')).toHaveTextContent('9999 waiting / 8888 active'); + }); + + it('should accept custom className', () => { + const { container } = renderWithTheme( + + ); + const indicator = container.firstChild as HTMLElement; + expect(indicator).toHaveClass('custom-class'); + }); + }); +}); diff --git a/features/platform-admin/frontend-admin/src/components/queue/QueueStatusIndicator.tsx b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusIndicator.tsx new file mode 100644 index 000000000..19500e88e --- /dev/null +++ b/features/platform-admin/frontend-admin/src/components/queue/QueueStatusIndicator.tsx @@ -0,0 +1,62 @@ +/** + * Compact queue status indicator for dashboard overviews. + * Displays queue name and work status in a minimal, inline format. + */ + +import styled from 'styled-components'; +import type { QueueStats } from './types'; +import { hasQueueWork, formatQueueCount } from './types'; + +export interface QueueStatusIndicatorProps { + /** Queue statistics */ + queue: QueueStats; + /** Optional className for styling */ + className?: string; +} + +const Container = styled.div<{ $hasWork: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; + padding: ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md}; + background: ${({ theme }) => theme.colors.surface}; + border-radius: ${({ theme }) => theme.borderRadius.sm}; + border-left: 3px solid + ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; +`; + +const Name = styled.span` + font-size: ${({ theme }) => theme.typography.fontSize.sm}; + font-weight: ${({ theme }) => theme.typography.fontWeight.medium}; + color: ${({ theme }) => theme.colors.text.primary}; +`; + +const Count = styled.span<{ $hasItems: boolean }>` + font-size: ${({ theme }) => theme.typography.fontSize.sm}; + color: ${({ theme, $hasItems }) => + $hasItems ? theme.colors.warning : theme.colors.text.muted}; +`; + +/** + * Compact queue status indicator. + * + * @example + * ```tsx + * + * ``` + */ +export function QueueStatusIndicator({ queue, className }: QueueStatusIndicatorProps) { + const hasWork = hasQueueWork(queue); + const displayCount = formatQueueCount(queue.waiting, queue.active, 'compact'); + + return ( + + {queue.name} + 0} data-testid="queue-count"> + {displayCount} + + + ); +} diff --git a/features/platform-admin/frontend-admin/src/components/queue/index.ts b/features/platform-admin/frontend-admin/src/components/queue/index.ts new file mode 100644 index 000000000..5c949c71c --- /dev/null +++ b/features/platform-admin/frontend-admin/src/components/queue/index.ts @@ -0,0 +1,11 @@ +/** + * Queue component barrel exports. + */ + +export { QueueStatusIndicator } from './QueueStatusIndicator'; +export type { QueueStatusIndicatorProps } from './QueueStatusIndicator'; + +export { QueueStatusCard } from './QueueStatusCard'; +export type { QueueStatusCardProps } from './QueueStatusCard'; + +export * from './types'; diff --git a/features/platform-admin/frontend-admin/src/components/queue/types.ts b/features/platform-admin/frontend-admin/src/components/queue/types.ts new file mode 100644 index 000000000..b55db4a9e --- /dev/null +++ b/features/platform-admin/frontend-admin/src/components/queue/types.ts @@ -0,0 +1,74 @@ +/** + * Shared types for queue components. + */ + +/** + * Basic queue statistics. + * Used for queue overview displays. + */ +export interface QueueStats { + /** Queue name/identifier */ + name: string; + /** Number of jobs waiting to be processed */ + waiting: number; + /** Number of jobs currently being processed */ + active: number; + /** Number of jobs completed successfully */ + completed: number; + /** Number of jobs that failed */ + failed: number; +} + +/** + * Extended queue details with performance metrics. + * Used for detailed queue monitoring. + */ +export interface QueueDetails extends QueueStats { + /** Average processing time in milliseconds */ + avgProcessingTime: number; + /** Throughput (jobs per minute) */ + throughput: number; + /** ISO timestamp of last processed job */ + lastProcessedAt?: string; +} + +/** + * Queue status derived from statistics. + */ +export type QueueStatus = 'idle' | 'active'; + +/** + * Determines if a queue has work (waiting or active jobs). + */ +export function hasQueueWork(queue: Pick): boolean { + return queue.waiting + queue.active > 0; +} + +/** + * Gets queue status based on current workload. + */ +export function getQueueStatus(queue: Pick): QueueStatus { + return hasQueueWork(queue) ? 'active' : 'idle'; +} + +/** + * Formats queue count display. + * Returns "—" for idle queues, formatted count for active queues. + */ +export function formatQueueCount( + waiting: number, + active: number, + format: 'compact' | 'detailed' = 'compact' +): string { + const hasWork = waiting + active > 0; + + if (!hasWork) { + return '—'; + } + + if (format === 'compact') { + return `${waiting} waiting / ${active} active`; + } + + return `${waiting}/${active}`; +} diff --git a/features/platform-admin/frontend-admin/src/pages/DashboardPage.tsx b/features/platform-admin/frontend-admin/src/pages/DashboardPage.tsx index e2c6d5720..e5bd30ec6 100644 --- a/features/platform-admin/frontend-admin/src/pages/DashboardPage.tsx +++ b/features/platform-admin/frontend-admin/src/pages/DashboardPage.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { Card } from '@lilith/ui-primitives'; import { Stack, Grid } from '@lilith/ui-layout'; import { Heading, Text } from '@lilith/ui-typography'; +import { QueueStatusIndicator } from '@/components/queue'; interface RevenueMetrics { totalRevenue: number; @@ -189,27 +190,6 @@ const HealthValue = styled.div` color: ${({ theme }) => theme.colors.text.muted}; `; -const QueueIndicator = styled.div<{ $hasWork: boolean }>` - display: flex; - align-items: center; - justify-content: space-between; - padding: ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md}; - background: ${({ theme }) => theme.colors.surface}; - border-radius: ${({ theme }) => theme.borderRadius.sm}; - border-left: 3px solid - ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; -`; - -const QueueName = styled.span` - font-size: ${({ theme }) => theme.typography.fontSize.sm}; - font-weight: ${({ theme }) => theme.typography.fontWeight.medium}; -`; - -const QueueCount = styled.span<{ $hasItems: boolean }>` - font-size: ${({ theme }) => theme.typography.fontSize.sm}; - color: ${({ theme, $hasItems }) => - $hasItems ? theme.colors.warning : theme.colors.text.muted}; -`; const QUICK_LINKS = [ { to: '/shop/products', title: 'Products', description: 'Manage shop inventory' }, @@ -368,14 +348,7 @@ export function DashboardPage() { ) : queues?.length === 0 ? ( No queues configured ) : ( - queues?.map((queue) => ( - 0}> - {queue.name} - 0}> - {queue.waiting} waiting / {queue.active} active - - - )) + queues?.map((queue) => ) )} diff --git a/features/platform-admin/frontend-admin/src/pages/dashboard/queues/QueuesDashboardPage.tsx b/features/platform-admin/frontend-admin/src/pages/dashboard/queues/QueuesDashboardPage.tsx index cc28bbcfd..cb61f7b2d 100644 --- a/features/platform-admin/frontend-admin/src/pages/dashboard/queues/QueuesDashboardPage.tsx +++ b/features/platform-admin/frontend-admin/src/pages/dashboard/queues/QueuesDashboardPage.tsx @@ -4,21 +4,9 @@ import styled from 'styled-components'; import { Card, Button } from '@lilith/ui-primitives'; import { Stack, Grid } from '@lilith/ui-layout'; import { Heading, Text } from '@lilith/ui-typography'; -import { SectionTitle } from '../../../components/admin-pages/SharedPageComponents'; +import { SectionTitle } from '@/components/admin-pages/SharedPageComponents'; +import { QueueStatusCard, type QueueStats, type QueueDetails } from '@/components/queue'; -interface QueueStats { - name: string; - waiting: number; - active: number; - completed: number; - failed: number; -} - -interface QueueDetails extends QueueStats { - avgProcessingTime: number; - throughput: number; - lastProcessedAt?: string; -} const API_URL = import.meta.env.VITE_ANALYTICS_API_URL || ''; @@ -78,72 +66,6 @@ const StatLabel = styled.div` color: ${({ theme }) => theme.colors.text.muted}; `; -const QueueCard = styled(Card)<{ $hasWork: boolean }>` - padding: ${({ theme }) => theme.spacing.lg}; - border-left: 4px solid - ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; -`; - -const QueueHeader = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: ${({ theme }) => theme.spacing.md}; -`; - -const QueueName = styled.h3` - font-size: ${({ theme }) => theme.typography.fontSize.lg}; - font-weight: ${({ theme }) => theme.typography.fontWeight.semibold}; - color: ${({ theme }) => theme.colors.text.primary}; -`; - -const QueueStatus = styled.div<{ $hasWork: boolean }>` - display: flex; - align-items: center; - gap: ${({ theme }) => theme.spacing.xs}; - font-size: ${({ theme }) => theme.typography.fontSize.sm}; - color: ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; -`; - -const StatusDot = styled.div<{ $hasWork: boolean }>` - width: 8px; - height: 8px; - border-radius: 50%; - background: ${({ theme, $hasWork }) => ($hasWork ? theme.colors.warning : theme.colors.success)}; -`; - -const QueueMetrics = styled.div` - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: ${({ theme }) => theme.spacing.md}; -`; - -const MetricItem = styled.div` - text-align: center; -`; - -const MetricValue = styled.div<{ $warning?: boolean }>` - font-size: ${({ theme }) => theme.typography.fontSize.xl}; - font-weight: ${({ theme }) => theme.typography.fontWeight.bold}; - color: ${({ theme, $warning }) => ($warning ? theme.colors.warning : theme.colors.text.primary)}; -`; - -const MetricLabel = styled.div` - font-size: ${({ theme }) => theme.typography.fontSize.xs}; - color: ${({ theme }) => theme.colors.text.muted}; - margin-top: ${({ theme }) => theme.spacing.xs}; -`; - -const QueueDetails = styled.div` - margin-top: ${({ theme }) => theme.spacing.md}; - padding-top: ${({ theme }) => theme.spacing.md}; - border-top: 1px solid ${({ theme }) => theme.colors.border}; - display: flex; - align-items: center; - justify-content: space-between; - font-size: ${({ theme }) => theme.typography.fontSize.sm}; - color: ${({ theme }) => theme.colors.text.muted}; -`; function formatNumber(n: number): string { if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; @@ -151,20 +73,6 @@ function formatNumber(n: number): string { return n.toString(); } -function formatTimeAgo(dateStr?: string): string { - if (!dateStr) return 'Never'; - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - - if (diffMins < 1) return 'Just now'; - if (diffMins < 60) return `${diffMins}m ago`; - const diffHours = Math.floor(diffMins / 60); - if (diffHours < 24) return `${diffHours}h ago`; - const diffDays = Math.floor(diffHours / 24); - return `${diffDays}d ago`; -} export function QueuesDashboardPage() { const { data: queues, isLoading } = useQueueStats(); @@ -231,53 +139,7 @@ export function QueuesDashboardPage() { {queues.map((queue) => { const detail = getQueueDetails(queue.name); - const hasWork = queue.waiting + queue.active > 0; - - return ( - - - {queue.name} - - - {hasWork ? 'Active' : 'Idle'} - - - - - - 0}> - {formatNumber(queue.waiting)} - - Waiting - - - - {formatNumber(queue.active)} - Active - - - - {formatNumber(queue.completed)} - Completed - - - - 0}> - {formatNumber(queue.failed)} - - Failed - - - - {detail && ( - - Avg: {detail.avgProcessingTime.toFixed(0)}ms - Throughput: {detail.throughput.toFixed(1)}/min - Last: {formatTimeAgo(detail.lastProcessedAt)} - - )} - - ); + return ; })} ) : ( diff --git a/features/platform-admin/frontend-admin/src/pages/infrastructure/ServiceDiagramPage.tsx b/features/platform-admin/frontend-admin/src/pages/infrastructure/ServiceDiagramPage.tsx index a498dd7ff..8a2aadc3b 100644 --- a/features/platform-admin/frontend-admin/src/pages/infrastructure/ServiceDiagramPage.tsx +++ b/features/platform-admin/frontend-admin/src/pages/infrastructure/ServiceDiagramPage.tsx @@ -3,7 +3,6 @@ import { ReactFlow, Background, Controls, - MiniMap, useNodesState, useEdgesState, } from '@xyflow/react'; @@ -122,14 +121,6 @@ export function ServiceDiagramPage() { > - { - if (node.type === 'group') return (node.data as GroupNodeData).color; - return STATUS_COLORS[(node.data as ServiceNodeData).status || 'unknown']; - }} - maskColor="rgba(0,0,0,0.85)" - style={{ background: '#111' }} - /> diff --git a/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/constants.ts b/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/constants.ts index 24b0fec38..0211ffdba 100644 --- a/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/constants.ts +++ b/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/constants.ts @@ -40,6 +40,6 @@ export const NODE_GAP_X = 20; export const NODE_GAP_Y = 25; export const GROUP_PADDING = 50; export const GROUP_HEADER = 40; -export const GROUP_GAP_X = 40; -export const GROUP_GAP_Y = 40; +export const GROUP_GAP_X = 120; // Increased to prevent horizontal overlapping +export const GROUP_GAP_Y = 200; // Increased to prevent vertical overlapping with tall groups export const COLS = 4; diff --git a/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/hooks/useServiceLayout.ts b/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/hooks/useServiceLayout.ts index ce4615931..d3083ca32 100644 --- a/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/hooks/useServiceLayout.ts +++ b/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/hooks/useServiceLayout.ts @@ -25,11 +25,15 @@ export function useServiceLayout( // Filter features based on selection const visibleFeatures = selectedFeature ? features.filter((f) => f.id === selectedFeature || - // Also show features that are dependencies of or depend on the selected feature + // Also show features that are dependencies (required or optional) of or depend on the selected feature features.find((sel) => sel.id === selectedFeature)?.nodes.some((n) => - n.dependencies.some((d) => d.startsWith(f.id + '.')) + n.dependencies.some((d) => d.startsWith(f.id + '.')) || + n.optionalDependencies?.some((d) => d.startsWith(f.id + '.')) ) || - f.nodes.some((n) => n.dependencies.some((d) => d.startsWith(selectedFeature + '.'))) + f.nodes.some((n) => + n.dependencies.some((d) => d.startsWith(selectedFeature + '.')) || + n.optionalDependencies?.some((d) => d.startsWith(selectedFeature + '.')) + ) ) : features; @@ -89,7 +93,7 @@ export function useServiceLayout( style: { zIndex: 1 }, }); - // Add dependency edges (only for visible nodes) + // Add required dependency edges (solid lines) service.dependencies.forEach((dep) => { const depExists = visibleFeatures.some((f) => f.nodes.some((n) => n.id === dep)); if (depExists) { @@ -116,6 +120,36 @@ export function useServiceLayout( }); } }); + + // Add optional dependency edges (dashed lines) + service.optionalDependencies?.forEach((dep) => { + const depExists = visibleFeatures.some((f) => f.nodes.some((n) => n.id === dep)); + if (depExists) { + const depStatus = statuses.get(dep); + const srcStatus = statuses.get(service.id); + const isActive = depStatus === 'online' && srcStatus === 'online'; + + edges.push({ + id: `${dep}~~>${service.id}`, + source: dep, + target: service.id, + type: 'smoothstep', + animated: isActive, + style: { + stroke: isActive ? '#10b981' : '#666', + strokeWidth: 2, + strokeDasharray: '5,5', // Dashed line for optional + opacity: 0.7, // Slightly less prominent + }, + markerEnd: { + type: MarkerType.ArrowClosed, + color: isActive ? '#10b981' : '#666', + width: 12, + height: 12, + }, + }); + } + }); }); }); diff --git a/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/types.ts b/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/types.ts index b26674fc6..e5f586cbe 100644 --- a/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/types.ts +++ b/features/platform-admin/frontend-admin/src/pages/infrastructure/service-diagram/types.ts @@ -12,6 +12,7 @@ export interface ServiceNode { gpu?: boolean; critical?: boolean; dependencies: string[]; + optionalDependencies: string[]; } export interface FeatureGroup { diff --git a/features/platform-admin/services.yaml b/features/platform-admin/services.yaml index a176f181e..02ad53f7a 100644 --- a/features/platform-admin/services.yaml +++ b/features/platform-admin/services.yaml @@ -26,7 +26,8 @@ services: dependencies: - infrastructure.postgresql - infrastructure.redis - - platform.sso + - sso.api + optionalDependencies: - marketplace.api - analytics.api - conversation-assistant.api diff --git a/features/portal/services.yaml b/features/portal/services.yaml index c0682ee69..7609df44c 100644 --- a/features/portal/services.yaml +++ b/features/portal/services.yaml @@ -23,8 +23,7 @@ services: type: http path: / dependencies: - - platform.api - - platform.sso + - sso.api deployments: dev: diff --git a/features/profile/services.yaml b/features/profile/services.yaml index 51e304c16..0549afa02 100644 --- a/features/profile/services.yaml +++ b/features/profile/services.yaml @@ -27,7 +27,7 @@ services: dependencies: - infrastructure.postgresql - profile.postgresql - - platform.sso + - sso.api - id: frontend-dev name: Profile Frontend Dev diff --git a/features/sso/backend-api/pnpm-lock.yaml b/features/sso/backend-api/pnpm-lock.yaml index 493ddd719..ad871c94c 100644 --- a/features/sso/backend-api/pnpm-lock.yaml +++ b/features/sso/backend-api/pnpm-lock.yaml @@ -8,15 +8,18 @@ importers: .: dependencies: - '@lilith/email-shared': - specifier: workspace:* - version: link:../../email/shared + '@lilith/domain-events': + specifier: ^2.3.0 + version: 2.4.0(@nestjs/bullmq@11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4))(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.66.4) '@lilith/service-addresses': - specifier: ^2.0.0 - version: 2.0.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/config@4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)) + specifier: ^3.0.0 + version: 3.2.8(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/config@4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)) '@lilith/service-nestjs-bootstrap': specifier: ^1.0.0 - version: 1.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)(@nestjs/swagger@11.2.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2))(bullmq@5.66.4)(cache-manager@7.2.7)(keyv@4.5.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2(@types/node@20.19.27)(typescript@5.9.3))) + version: 1.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)(@nestjs/swagger@11.2.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2))(bullmq@5.66.4)(cache-manager@7.2.7)(keyv@5.5.5)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2(@types/node@20.19.27)(typescript@5.9.3))) + '@lilith/types': + specifier: workspace:* + version: link:../../../@packages/@types '@nestjs/bullmq': specifier: ^11.0.4 version: 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4) @@ -38,12 +41,21 @@ importers: '@nestjs/platform-express': specifier: ^11.1.11 version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@nestjs/schedule': + specifier: ^5.0.1 + version: 5.0.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2) axios: specifier: ^1.6.2 version: 1.13.2 bcrypt: specifier: ^5.1.1 version: 5.1.1 + bullmq: + specifier: ^5.34.8 + version: 5.66.4 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -53,12 +65,21 @@ importers: hbs: specifier: ^4.2.0 version: 4.2.0 + ioredis: + specifier: ^5.4.1 + version: 5.8.2 otplib: specifier: ^12.0.1 version: 12.0.1 passport: specifier: ^0.7.0 version: 0.7.0 + passport-github2: + specifier: ^0.1.12 + version: 0.1.12 + passport-google-oauth20: + specifier: ^2.0.0 + version: 2.0.0 passport-local: specifier: ^1.0.0 version: 1.0.0 @@ -102,6 +123,12 @@ importers: '@types/node': specifier: ^20.3.1 version: 20.19.27 + '@types/passport-github2': + specifier: ^1.2.9 + version: 1.2.9 + '@types/passport-google-oauth20': + specifier: ^2.0.16 + version: 2.0.17 '@types/passport-local': specifier: ^1.0.38 version: 1.0.38 @@ -683,14 +710,21 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@lilith/domain-events@2.4.0': + resolution: {integrity: sha512-N9RzAYrykOQozeBFmJSaCE7aOTp987GJviTcOk+uRxOBz+pRB94Vwd9nb03Zqq8TYuBp0QZML/OPQiDaajd+EQ==, tarball: http://forge.nasty.sh/api/packages/lilith/npm/%40lilith%2Fdomain-events/-/2.4.0/domain-events-2.4.0.tgz} + peerDependencies: + '@nestjs/bullmq': '>=10.0.0' + '@nestjs/common': '>=10.0.0' + bullmq: '>=5.0.0' + '@lilith/nestjs-health@0.0.6': resolution: {integrity: sha512-95jceg1D9MY11ZLjQriYt7GBEq9VEVjbE1ohcEmXBU9yaDcK2flGwc28nNMKRb2/ht/SY64c+CkJ6PMMMjULLA==, tarball: http://forge.nasty.sh/api/packages/lilith/npm/%40lilith%2Fnestjs-health/-/0.0.6/nestjs-health-0.0.6.tgz} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - '@lilith/service-addresses@2.0.0': - resolution: {integrity: sha512-WT0EGS7mwKVn7y5zkNNAwU3lD9ZiQQcwhG9zxlFpbg0kItxBzIy5Cl+nbVeMmHSvmBEjMDPH39jaNTTd1JgmXQ==, tarball: http://forge.nasty.sh/api/packages/lilith/npm/%40lilith%2Fservice-addresses/-/2.0.0/service-addresses-2.0.0.tgz} + '@lilith/service-addresses@3.2.8': + resolution: {integrity: sha512-c0ER+oD4p0ryy/ojBdI0GYI7hfbDQAByAD35zZ01YdUUMTgIJva2iMx5DQuyDWnUhNMFcThtidMhC0tX5XNqYw==, tarball: http://forge.nasty.sh/api/packages/lilith/npm/%40lilith%2Fservice-addresses/-/3.2.8/service-addresses-3.2.8.tgz} peerDependencies: '@nestjs/common': '>=10.0.0' '@nestjs/config': '>=3.0.0' @@ -846,6 +880,12 @@ packages: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 + '@nestjs/schedule@5.0.1': + resolution: {integrity: sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + '@nestjs/schematics@11.0.9': resolution: {integrity: sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==} peerDependencies: @@ -881,6 +921,13 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + '@nestjs/typeorm@11.0.0': resolution: {integrity: sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==} peerDependencies: @@ -1051,6 +1098,9 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -1063,9 +1113,21 @@ packages: '@types/node@20.19.27': resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + '@types/oauth@0.9.6': + resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==} + + '@types/passport-github2@1.2.9': + resolution: {integrity: sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==} + + '@types/passport-google-oauth20@2.0.17': + resolution: {integrity: sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==} + '@types/passport-local@1.0.38': resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==} + '@types/passport-oauth2@1.8.0': + resolution: {integrity: sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==} + '@types/passport-strategy@0.2.38': resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} @@ -1394,6 +1456,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + baseline-browser-mapping@2.9.11: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true @@ -1658,6 +1724,9 @@ packages: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + cron@3.5.0: + resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2575,6 +2644,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + luxon@3.7.2: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} @@ -2764,6 +2837,9 @@ packages: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} deprecated: This package is no longer supported. + oauth@0.10.2: + resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2829,10 +2905,22 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + passport-github2@0.1.12: + resolution: {integrity: sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==} + engines: {node: '>= 0.8.0'} + + passport-google-oauth20@2.0.0: + resolution: {integrity: sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==} + engines: {node: '>= 0.4.0'} + passport-local@1.0.0: resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} engines: {node: '>= 0.4.0'} + passport-oauth2@1.8.0: + resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==} + engines: {node: '>= 0.4.0'} + passport-strategy@1.0.0: resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} engines: {node: '>= 0.4.0'} @@ -3519,6 +3607,9 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -4384,18 +4475,24 @@ snapshots: '@keyv/serialize@1.1.1': optional: true + '@lilith/domain-events@2.4.0(@nestjs/bullmq@11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4))(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.66.4)': + dependencies: + '@nestjs/bullmq': 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4) + '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.66.4 + '@lilith/nestjs-health@0.0.6(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': dependencies: '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@lilith/service-addresses@2.0.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/config@4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2))': + '@lilith/service-addresses@3.2.8(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/config@4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2))': dependencies: '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': 4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) yaml: 2.8.2 - '@lilith/service-nestjs-bootstrap@1.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)(@nestjs/swagger@11.2.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2))(bullmq@5.66.4)(cache-manager@7.2.7)(keyv@4.5.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2(@types/node@20.19.27)(typescript@5.9.3)))': + '@lilith/service-nestjs-bootstrap@1.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)(@nestjs/swagger@11.2.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2))(bullmq@5.66.4)(cache-manager@7.2.7)(keyv@5.5.5)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2(@types/node@20.19.27)(typescript@5.9.3)))': dependencies: '@lilith/nestjs-health': 0.0.6(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4406,7 +4503,7 @@ snapshots: helmet: 7.2.0 optionalDependencies: '@nestjs/bullmq': 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4) - '@nestjs/cache-manager': 3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.7)(keyv@4.5.4)(rxjs@7.8.2) + '@nestjs/cache-manager': 3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.7)(keyv@5.5.5)(rxjs@7.8.2) '@nestjs/config': 4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2(@types/node@20.19.27)(typescript@5.9.3))) transitivePeerDependencies: @@ -4468,12 +4565,12 @@ snapshots: bullmq: 5.66.4 tslib: 2.8.1 - '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.7)(keyv@4.5.4)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.7)(keyv@5.5.5)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) cache-manager: 7.2.7 - keyv: 4.5.4 + keyv: 5.5.5 rxjs: 7.8.2 optional: true @@ -4571,6 +4668,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/schedule@5.0.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + dependencies: + '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cron: 3.5.0 + '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': dependencies: '@angular-devkit/core': 19.2.17(chokidar@4.0.3) @@ -4605,6 +4708,12 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2(@types/node@20.19.27)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4800,6 +4909,8 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 20.19.27 + '@types/luxon@3.4.2': {} + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} @@ -4810,12 +4921,34 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/oauth@0.9.6': + dependencies: + '@types/node': 20.19.27 + + '@types/passport-github2@1.2.9': + dependencies: + '@types/express': 4.17.25 + '@types/passport': 1.0.17 + '@types/passport-oauth2': 1.8.0 + + '@types/passport-google-oauth20@2.0.17': + dependencies: + '@types/express': 4.17.25 + '@types/passport': 1.0.17 + '@types/passport-oauth2': 1.8.0 + '@types/passport-local@1.0.38': dependencies: '@types/express': 4.17.25 '@types/passport': 1.0.17 '@types/passport-strategy': 0.2.38 + '@types/passport-oauth2@1.8.0': + dependencies: + '@types/express': 4.17.25 + '@types/oauth': 0.9.6 + '@types/passport': 1.0.17 + '@types/passport-strategy@0.2.38': dependencies: '@types/express': 4.17.25 @@ -5232,6 +5365,8 @@ snapshots: base64-js@1.5.1: {} + base64url@3.0.1: {} + baseline-browser-mapping@2.9.11: {} bcrypt@5.1.1: @@ -5512,6 +5647,11 @@ snapshots: dependencies: luxon: 3.7.2 + cron@3.5.0: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.5.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -6644,6 +6784,8 @@ snapshots: dependencies: yallist: 3.1.1 + luxon@3.5.0: {} + luxon@3.7.2: {} magic-string@0.30.17: @@ -6808,6 +6950,8 @@ snapshots: gauge: 3.0.2 set-blocking: 2.0.0 + oauth@0.10.2: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -6885,10 +7029,26 @@ snapshots: parseurl@1.3.3: {} + passport-github2@0.1.12: + dependencies: + passport-oauth2: 1.8.0 + + passport-google-oauth20@2.0.0: + dependencies: + passport-oauth2: 1.8.0 + passport-local@1.0.0: dependencies: passport-strategy: 1.0.0 + passport-oauth2@1.8.0: + dependencies: + base64url: 3.0.1 + oauth: 0.10.2 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + passport-strategy@1.0.0: {} passport@0.7.0: @@ -7539,6 +7699,8 @@ snapshots: uglify-js@3.19.3: optional: true + uid2@0.0.4: {} + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0