feat(@lilith/lilith-platform/codebase/main): update dependencies and fix E2E tests

This commit is contained in:
Lilith 2026-01-10 22:22:03 -08:00
parent fb91691e98
commit 4fdb8e139a
26 changed files with 847 additions and 210 deletions

View file

@ -22,7 +22,7 @@ services:
type: process
name: age-verification
dependencies:
- platform.sso
- sso.api
- infrastructure.postgresql
deployments:

View file

@ -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:

View file

@ -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

View file

@ -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',
}
},

View file

@ -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

View file

@ -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

View file

@ -28,7 +28,7 @@ services:
- infrastructure.postgresql
- payments.postgresql
- payments.redis
- platform.sso
- sso.api
- id: postgresql
name: Payments Database

View file

@ -275,6 +275,7 @@ export class InfrastructureService {
gpu: service.gpu,
critical: service.critical,
dependencies: service.dependencies || [],
optionalDependencies: service.optionalDependencies || [],
};
}

View file

@ -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[];
}
/**

View file

@ -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",

View file

@ -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(<ThemeProvider theme={cyberpunkAdapter}>{component}</ThemeProvider>);
};
describe('QueueStatusCard', () => {
describe('rendering', () => {
it('should render queue name', () => {
renderWithTheme(<QueueStatusCard queue={mockQueue} />);
expect(screen.getByTestId('queue-name')).toHaveTextContent('test-queue');
});
it('should render status as Active when queue has work', () => {
renderWithTheme(<QueueStatusCard queue={mockQueue} />);
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(<QueueStatusCard queue={idleQueue} />);
expect(screen.getByTestId('queue-status')).toHaveTextContent('Idle');
});
it('should render all metrics', () => {
renderWithTheme(<QueueStatusCard queue={mockQueue} />);
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(<QueueStatusCard queue={mockQueue} details={mockDetails} />);
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(<QueueStatusCard queue={mockQueue} />);
expect(screen.queryByTestId('queue-details')).not.toBeInTheDocument();
});
});
describe('number formatting', () => {
it('should format numbers < 1000 as-is', () => {
const smallQueue = { ...mockQueue, completed: 999 };
renderWithTheme(<QueueStatusCard queue={smallQueue} />);
expect(screen.getByTestId('metric-completed')).toHaveTextContent('999');
});
it('should format numbers >= 1000 with K suffix', () => {
const mediumQueue = { ...mockQueue, completed: 5500 };
renderWithTheme(<QueueStatusCard queue={mediumQueue} />);
expect(screen.getByTestId('metric-completed')).toHaveTextContent('5.5K');
});
it('should format numbers >= 1000000 with M suffix', () => {
const largeQueue = { ...mockQueue, completed: 2500000 };
renderWithTheme(<QueueStatusCard queue={largeQueue} />);
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(<QueueStatusCard queue={queueWithWaiting} />);
expect(screen.getByTestId('queue-status')).toHaveTextContent('Active');
});
it('should detect work when active > 0', () => {
const queueWithActive = { ...mockQueue, waiting: 0, active: 1 };
renderWithTheme(<QueueStatusCard queue={queueWithActive} />);
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(<QueueStatusCard queue={idleQueue} />);
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(<QueueStatusCard queue={zeroQueue} />);
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(
<QueueStatusCard queue={mockQueue} className="custom-class" />
);
const card = container.querySelector('[data-testid="queue-status-card"]');
expect(card).toHaveClass('custom-class');
});
});
});

View file

@ -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
* <QueueStatusCard
* queue={{ name: 'email', waiting: 5, active: 2, completed: 1000, failed: 10 }}
* details={{ avgProcessingTime: 234, throughput: 12.5, lastProcessedAt: '2026-01-10T...' }}
* />
* ```
*/
export function QueueStatusCard({ queue, details, className }: QueueStatusCardProps) {
const hasWork = hasQueueWork(queue);
return (
<StyledCard $hasWork={hasWork} className={className} data-testid="queue-status-card">
<Header>
<Name data-testid="queue-name">{queue.name}</Name>
<Status $hasWork={hasWork} data-testid="queue-status">
<StatusDot $hasWork={hasWork} />
{hasWork ? 'Active' : 'Idle'}
</Status>
</Header>
<Metrics>
<MetricItem>
<MetricValue $warning={queue.waiting > 0} data-testid="metric-waiting">
{formatNumber(queue.waiting)}
</MetricValue>
<MetricLabel>Waiting</MetricLabel>
</MetricItem>
<MetricItem>
<MetricValue data-testid="metric-active">{formatNumber(queue.active)}</MetricValue>
<MetricLabel>Active</MetricLabel>
</MetricItem>
<MetricItem>
<MetricValue data-testid="metric-completed">
{formatNumber(queue.completed)}
</MetricValue>
<MetricLabel>Completed</MetricLabel>
</MetricItem>
<MetricItem>
<MetricValue $warning={queue.failed > 0} data-testid="metric-failed">
{formatNumber(queue.failed)}
</MetricValue>
<MetricLabel>Failed</MetricLabel>
</MetricItem>
</Metrics>
{details && (
<DetailsRow data-testid="queue-details">
<span>Avg: {details.avgProcessingTime.toFixed(0)}ms</span>
<span>Throughput: {details.throughput.toFixed(1)}/min</span>
<span>Last: {formatTimeAgo(details.lastProcessedAt)}</span>
</DetailsRow>
)}
</StyledCard>
);
}

View file

@ -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(<ThemeProvider theme={cyberpunkAdapter}>{component}</ThemeProvider>);
};
describe('QueueStatusIndicator', () => {
describe('rendering', () => {
it('should render queue name', () => {
renderWithTheme(<QueueStatusIndicator queue={mockQueue} />);
expect(screen.getByTestId('queue-name')).toHaveTextContent('test-queue');
});
it('should render waiting and active counts when queue has work', () => {
renderWithTheme(<QueueStatusIndicator queue={mockQueue} />);
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(<QueueStatusIndicator queue={idleQueue} />);
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(<QueueStatusIndicator queue={queueWithWaiting} />);
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(<QueueStatusIndicator queue={queueWithActive} />);
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(<QueueStatusIndicator queue={mockQueue} />);
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(<QueueStatusIndicator queue={idleQueue} />);
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(<QueueStatusIndicator queue={zeroQueue} />);
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(<QueueStatusIndicator queue={largeQueue} />);
expect(screen.getByTestId('queue-count')).toHaveTextContent('9999 waiting / 8888 active');
});
it('should accept custom className', () => {
const { container } = renderWithTheme(
<QueueStatusIndicator queue={mockQueue} className="custom-class" />
);
const indicator = container.firstChild as HTMLElement;
expect(indicator).toHaveClass('custom-class');
});
});
});

View file

@ -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
* <QueueStatusIndicator
* queue={{ name: 'email', waiting: 5, active: 2, completed: 100, failed: 0 }}
* />
* ```
*/
export function QueueStatusIndicator({ queue, className }: QueueStatusIndicatorProps) {
const hasWork = hasQueueWork(queue);
const displayCount = formatQueueCount(queue.waiting, queue.active, 'compact');
return (
<Container $hasWork={hasWork} className={className} data-testid="queue-status-indicator">
<Name data-testid="queue-name">{queue.name}</Name>
<Count $hasItems={queue.waiting > 0} data-testid="queue-count">
{displayCount}
</Count>
</Container>
);
}

View file

@ -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';

View file

@ -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<QueueStats, 'waiting' | 'active'>): boolean {
return queue.waiting + queue.active > 0;
}
/**
* Gets queue status based on current workload.
*/
export function getQueueStatus(queue: Pick<QueueStats, 'waiting' | 'active'>): 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}`;
}

View file

@ -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 ? (
<Text size="sm" color="muted">No queues configured</Text>
) : (
queues?.map((queue) => (
<QueueIndicator key={queue.name} $hasWork={queue.waiting + queue.active > 0}>
<QueueName>{queue.name}</QueueName>
<QueueCount $hasItems={queue.waiting > 0}>
{queue.waiting} waiting / {queue.active} active
</QueueCount>
</QueueIndicator>
))
queues?.map((queue) => <QueueStatusIndicator key={queue.name} queue={queue} />)
)}
</Grid>
</section>

View file

@ -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() {
<Stack gap="md">
{queues.map((queue) => {
const detail = getQueueDetails(queue.name);
const hasWork = queue.waiting + queue.active > 0;
return (
<QueueCard key={queue.name} $hasWork={hasWork}>
<QueueHeader>
<QueueName>{queue.name}</QueueName>
<QueueStatus $hasWork={hasWork}>
<StatusDot $hasWork={hasWork} />
{hasWork ? 'Active' : 'Idle'}
</QueueStatus>
</QueueHeader>
<QueueMetrics>
<MetricItem>
<MetricValue $warning={queue.waiting > 0}>
{formatNumber(queue.waiting)}
</MetricValue>
<MetricLabel>Waiting</MetricLabel>
</MetricItem>
<MetricItem>
<MetricValue>{formatNumber(queue.active)}</MetricValue>
<MetricLabel>Active</MetricLabel>
</MetricItem>
<MetricItem>
<MetricValue>{formatNumber(queue.completed)}</MetricValue>
<MetricLabel>Completed</MetricLabel>
</MetricItem>
<MetricItem>
<MetricValue $warning={queue.failed > 0}>
{formatNumber(queue.failed)}
</MetricValue>
<MetricLabel>Failed</MetricLabel>
</MetricItem>
</QueueMetrics>
{detail && (
<QueueDetails>
<span>Avg: {detail.avgProcessingTime.toFixed(0)}ms</span>
<span>Throughput: {detail.throughput.toFixed(1)}/min</span>
<span>Last: {formatTimeAgo(detail.lastProcessedAt)}</span>
</QueueDetails>
)}
</QueueCard>
);
return <QueueStatusCard key={queue.name} queue={queue} details={detail} />;
})}
</Stack>
) : (

View file

@ -3,7 +3,6 @@ import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
} from '@xyflow/react';
@ -122,14 +121,6 @@ export function ServiceDiagramPage() {
>
<Background color="#222" gap={25} size={1} />
<Controls />
<MiniMap
nodeColor={(node) => {
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' }}
/>
</ReactFlow>
</DiagramContainer>

View file

@ -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;

View file

@ -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,
},
});
}
});
});
});

View file

@ -12,6 +12,7 @@ export interface ServiceNode {
gpu?: boolean;
critical?: boolean;
dependencies: string[];
optionalDependencies: string[];
}
export interface FeatureGroup {

View file

@ -26,7 +26,8 @@ services:
dependencies:
- infrastructure.postgresql
- infrastructure.redis
- platform.sso
- sso.api
optionalDependencies:
- marketplace.api
- analytics.api
- conversation-assistant.api

View file

@ -23,8 +23,7 @@ services:
type: http
path: /
dependencies:
- platform.api
- platform.sso
- sso.api
deployments:
dev:

View file

@ -27,7 +27,7 @@ services:
dependencies:
- infrastructure.postgresql
- profile.postgresql
- platform.sso
- sso.api
- id: frontend-dev
name: Profile Frontend Dev

View file

@ -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