Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 40 additions & 17 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ on:
TEST_PASSWORD:
description: 'Password for test authentication'
required: false
AI_API_KEY:
description: 'API key for AI'
required: false
AI_PROXY_URL:
description: 'Proxy URL for AI'
required: false
outputs:
test-results:
description: 'Test results summary'
Expand Down Expand Up @@ -95,7 +101,6 @@ jobs:
secret-ids: |
SUPABASE, /cloudops/managed-secrets/cloud/supabase/api_key
AUTH0, /cloudops/managed-secrets/auth0/LFX_V2_PCC

- name: Setup Turborepo cache
uses: actions/cache@v4
with:
Expand All @@ -115,19 +120,37 @@ jobs:
# Check AWS Secrets Manager secrets (masked environment variables)
if [ -z "$AUTH0" ]; then
missing_secrets="$missing_secrets AUTH0 (from AWS Secrets Manager)"
else
AUTH0_CLIENT_ID=$(echo "$AUTH0" | jq -r '.client_id // empty')
AUTH0_CLIENT_SECRET=$(echo "$AUTH0" | jq -r '.client_secret // empty')
if [ -z "$AUTH0_CLIENT_ID" ] || [ -z "$AUTH0_CLIENT_SECRET" ]; then
missing_secrets="$missing_secrets AUTH0.client_id or AUTH0.client_secret (from AWS Secrets Manager)"
fi
fi
if [ -z "$SUPABASE" ]; then
missing_secrets="$missing_secrets SUPABASE (from AWS Secrets Manager)"
else
SUPABASE_URL=$(echo "$SUPABASE" | jq -r '.url // empty')
SUPABASE_API_KEY=$(echo "$SUPABASE" | jq -r '.api_key // empty')
if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_API_KEY" ]; then
missing_secrets="$missing_secrets SUPABASE.url or SUPABASE.api_key (from AWS Secrets Manager)"
fi
fi

# Check GitHub secrets (fallback)
if [ -z "${{ secrets.TEST_USERNAME }}" ]; then
missing_secrets="$missing_secrets TEST_USERNAME"
fi
if [ -z "${{ secrets.TEST_PASSWORD }}" ]; then
missing_secrets="$missing_secrets TEST_PASSWORD"
fi

if [ -z "${{ secrets.AI_API_KEY }}" ]; then
missing_secrets="$missing_secrets AI_API_KEY"
fi
if [ -z "${{ secrets.AI_PROXY_URL }}" ]; then
missing_secrets="$missing_secrets AI_PROXY_URL"
fi

if [ -n "$missing_secrets" ]; then
echo "❌ Missing required secrets for E2E testing:$missing_secrets"
echo "Please configure these secrets to enable E2E tests."
Expand All @@ -145,6 +168,7 @@ jobs:
echo "PCC_AUTH0_ISSUER_BASE_URL=https://linuxfoundation-dev.auth0.com/" >> $GITHUB_ENV
echo "PCC_AUTH0_AUDIENCE=https://api-gw.dev.platform.linuxfoundation.org/" >> $GITHUB_ENV
echo "LFX_V2_SERVICE=http://lfx-api.dev.v2.cluster.linuxfound.info" >> $GITHUB_ENV
echo "NATS_URL=nats://lfx-platform-nats.lfx.svc.cluster.local:4222" >> $GITHUB_ENV
echo "CI=true" >> $GITHUB_ENV

- name: Set up sensitive environment variables
Expand All @@ -164,27 +188,35 @@ jobs:
echo "PCC_AUTH0_CLIENT_SECRET=$AUTH0_CLIENT_SECRET" >> $GITHUB_ENV
echo "✅ AUTH0 secrets set as masked environment variables"
fi

# Parse and set SUPABASE secrets
if [ -n "$SUPABASE" ]; then
SUPABASE_URL=$(echo "$SUPABASE" | jq -r '.url // empty')
SUPABASE_API_KEY=$(echo "$SUPABASE" | jq -r '.api_key // empty')

# Explicitly mask the values
echo "::add-mask::$SUPABASE_URL"
echo "::add-mask::$SUPABASE_API_KEY"

# Set as environment variables
echo "SUPABASE_URL=$SUPABASE_URL" >> $GITHUB_ENV
echo "POSTGRES_API_KEY=$SUPABASE_API_KEY" >> $GITHUB_ENV
echo "✅ SUPABASE secrets set as masked environment variables"
fi


# Set AI secrets
echo "::add-mask::${{ secrets.AI_API_KEY }}"
echo "::add-mask::${{ secrets.AI_PROXY_URL }}"
echo "AI_API_KEY=${{ secrets.AI_API_KEY }}" >> $GITHUB_ENV
echo "AI_PROXY_URL=${{ secrets.AI_PROXY_URL }}" >> $GITHUB_ENV
echo "✅ AI secrets set as masked environment variables"

# Set test credentials
echo "::add-mask::${{ secrets.TEST_USERNAME }}"
echo "::add-mask::${{ secrets.TEST_PASSWORD }}"
echo "TEST_USERNAME=${{ secrets.TEST_USERNAME }}" >> $GITHUB_ENV
echo "TEST_PASSWORD=${{ secrets.TEST_PASSWORD }}" >> $GITHUB_ENV
echo "✅ TEST_USERNAME and TEST_PASSWORD secrets set as masked environment variables"

- name: Install Playwright browsers
if: steps.validate-secrets.outputs.can_run_tests == 'true'
Expand Down Expand Up @@ -239,6 +271,7 @@ jobs:
echo "AWS Secrets Manager (required):"
echo " - /cloudops/managed-secrets/auth0/LFX_V2_PCC (AUTH0 configuration)"
echo " - /cloudops/managed-secrets/cloud/supabase/api_key (SUPABASE configuration)"
echo " - /cloudops/managed-secrets/ai/ai_config (AI configuration)"
echo ""
echo "GitHub Secrets (required for authenticated tests):"
echo " - TEST_USERNAME"
Expand All @@ -263,16 +296,6 @@ jobs:
echo "results=failure" >> $GITHUB_OUTPUT
fi

- name: Upload Playwright report
id: upload-report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ inputs.browser }}-${{ github.run_id }}
path: |
apps/lfx-pcc/playwright-report/
retention-days: 7

- name: Comment test results on PR
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/quality-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ jobs:
secrets:
TEST_USERNAME: ${{ secrets.TEST_USERNAME }}
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
AI_API_KEY: ${{ secrets.AI_API_KEY }}
AI_PROXY_URL: ${{ secrets.AI_PROXY_URL }}
2 changes: 2 additions & 0 deletions .github/workflows/weekly-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ jobs:
secrets:
TEST_USERNAME: ${{ secrets.TEST_USERNAME }}
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
AI_API_KEY: ${{ secrets.AI_API_KEY }}
AI_PROXY_URL: ${{ secrets.AI_PROXY_URL }}

report-results:
name: Report Weekly Test Results
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"iconfield",
"inputicon",
"Linkify",
"litellm",
"networkidle",
"nonexistentproject",
"PostgreSQL",
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ LFX PCC is a Turborepo monorepo containing an Angular 19 SSR application with ex
## Monorepo Structure

```text
lfx-pcc-v3/
lfx-v2-pcc-ui/
├── apps/
│ └── lfx-pcc/ # Angular 19 SSR application with zoneless change detection
│ ├── eslint.config.mjs # Angular-specific ESLint rules
Expand Down Expand Up @@ -96,6 +96,8 @@ lfx-pcc-v3/
- Logging uses Pino for structured JSON logs with sensitive data redaction
- Health checks are available at /health and are not logged or authenticated
- All shared types, interfaces, and constants are centralized in @lfx-pcc/shared package
- **AI Service Integration**: Claude Sonnet 4 model via LiteLLM proxy for meeting agenda generation
- **AI Environment Variables**: AI_PROXY_URL and AI_API_KEY required for AI functionality
- Use TypeScript interfaces instead of union types for better maintainability
- Shared package uses direct source imports during development for hot reloading
- **Interfaces go into the shared packages**
Expand Down
5 changes: 5 additions & 0 deletions apps/lfx-pcc/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ SUPABASE_STORAGE_BUCKET=your-supabase-bucket-name
# Internal k8s service DNS for NATS cluster
NATS_URL=nats://lfx-platform-nats.lfx.svc.cluster.local:4222

# AI Service Configuration
# OpenAI-compatible proxy for meeting agenda generation
AI_PROXY_URL=https://litellm.tools.lfx.dev/chat/completions
AI_API_KEY=your-litellm-ai-api-key

# E2E Test Configuration (Optional)
# Test user credentials for automated testing
TEST_USERNAME=your-test-username
Expand Down
4 changes: 4 additions & 0 deletions apps/lfx-pcc/e2e/homepage-robust.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

import { expect, test } from '@playwright/test';

import { ApiMockHelper } from './helpers/api-mock.helper';

test.describe('Homepage - Robust Tests', () => {
test.beforeEach(async ({ page }) => {
await ApiMockHelper.setupProjectSlugMock(page);

await page.goto('/', { waitUntil: 'domcontentloaded' });

// Verify we're authenticated and on the homepage
Expand Down
12 changes: 3 additions & 9 deletions apps/lfx-pcc/e2e/homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { ApiMockHelper } from './helpers/api-mock.helper';

test.describe('Homepage', () => {
test.beforeEach(async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

await page.goto('/', { waitUntil: 'domcontentloaded' });

// Verify we're authenticated and on the homepage
Expand Down Expand Up @@ -114,9 +117,6 @@ test.describe('Homepage', () => {
});

test('should filter projects when searching', async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Wait for project cards to appear
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });

Expand Down Expand Up @@ -149,9 +149,6 @@ test.describe('Homepage', () => {
});

test('should clear search and show all projects', async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Wait for project cards to appear
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });

Expand All @@ -175,9 +172,6 @@ test.describe('Homepage', () => {
});

test('should navigate to project detail when clicking a project card', async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Wait for project cards to appear
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });

Expand Down
21 changes: 5 additions & 16 deletions apps/lfx-pcc/e2e/project-dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ test.describe('Project Dashboard', () => {
});

test('should display all navigation tabs', async ({ page }) => {
console.log(await page.getByTestId('menu-item').allInnerTexts());
await expect(page.getByTestId('menu-item').filter({ hasText: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Meetings' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Committees' })).toBeVisible();
Expand All @@ -93,16 +92,6 @@ test.describe('Project Dashboard', () => {
const projectImage = page.locator('img').first();
await expect(projectImage).toBeVisible();
});

test('should display project summary counts', async ({ page }) => {
// Look for summary cards in the upper section of the page
await expect(page.locator('span').filter({ hasText: 'Meetings' }).first()).toBeVisible();
await expect(page.locator('span').filter({ hasText: 'Committees' }).first()).toBeVisible();
await expect(page.locator('span').filter({ hasText: 'Mailing Lists' }).first()).toBeVisible();

// Check for count values (they should be visible as numbers)
await expect(page.getByText(/^\d+$/).first()).toBeVisible();
});
});

test.describe('Metrics Cards', () => {
Expand Down Expand Up @@ -191,16 +180,16 @@ test.describe('Project Dashboard', () => {
});

test('should display all quick action items', async ({ page }) => {
await expect(page.getByRole('menuitem', { name: 'Schedule Meeting' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Create Meeting' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Create Committee' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'View All Committees' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'View Calendar' })).toBeVisible();
});

test('should have working links in quick actions', async ({ page }) => {
// Schedule Meeting should link to meetings page
const scheduleMeetingLink = page.getByRole('link', { name: 'Schedule Meeting' });
await expect(scheduleMeetingLink).toHaveAttribute('href', /\/meetings$/);
// Create Meeting should link to meetings page
const createMeetingLink = page.getByRole('link', { name: 'Create Meeting' });
await expect(createMeetingLink).toHaveAttribute('href', /\/meetings\/create$/);

// View All Committees should link to committees page
const viewCommitteesLink = page.getByRole('link', { name: 'View All Committees' });
Expand All @@ -216,7 +205,7 @@ test.describe('Project Dashboard', () => {
const quickActionsSection = page.getByText('Quick Actions').locator('..');

// Check that menu items are present and interactive
await expect(quickActionsSection.getByRole('menuitem', { name: 'Schedule Meeting' })).toBeVisible();
await expect(quickActionsSection.getByRole('menuitem', { name: 'Create Meeting' })).toBeVisible();
await expect(quickActionsSection.getByRole('menuitem', { name: 'Create Committee' })).toBeVisible();
await expect(quickActionsSection.getByRole('menuitem', { name: 'View All Committees' })).toBeVisible();
await expect(quickActionsSection.getByRole('menuitem', { name: 'View Calendar' })).toBeVisible();
Expand Down
4 changes: 4 additions & 0 deletions apps/lfx-pcc/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
}
}

.p-textarea {
field-sizing: content;
}

.pill {
@apply inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-full transition-colors text-gray-600 border border-gray-200 hover:bg-gray-50;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,11 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">

<!-- Project Description -->
@if (projectDescription(); as description) {
<p class="text-gray-600 text-sm mb-6 max-w-2xl leading-relaxed">
<p class="text-gray-600 text-sm mb-6 max-w-3xl leading-relaxed">
{{ description }}
</p>
}
</div>
<!-- Metrics Section -->
@if (metrics().length > 0) {
<div class="flex items-center gap-8 mb-8 flex-wrap">
@for (metric of metrics(); track metric.label) {
<div class="flex flex-col">
<div class="flex items-center gap-2 mb-1" data-testid="metric-label">
<i [class]="metric.icon" class="text-sm"></i>
<span class="text-sm text-gray-600">{{ metric.label }}</span>
</div>
<div class="text-2xl font-normal text-gray-900 font-display" data-testid="metric-value">{{ metric.value }}</div>
</div>
}
</div>
}
</div>

<!-- Menu Items Section -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,11 @@ <h3 class="text-sm font-display text-gray-500">Upcoming Meetings</h3>
<h3 class="text-lg font-medium text-gray-900 mb-2">No Upcoming Meetings</h3>
<p class="text-gray-600 mb-4 text-sm">There are no upcoming meetings scheduled.</p>
<lfx-button
label="Schedule Meeting"
label="Create Meeting"
icon="fa-light fa-calendar-plus"
severity="secondary"
size="small"
[routerLink]="['meetings']"
[routerLink]="['meetings/create']"
[relativeTo]="activatedRoute">
</lfx-button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,9 +541,9 @@ export class ProjectComponent {
private initializeQuickActionMenuItems(): MenuItem[] {
return [
{
label: 'Schedule Meeting',
label: 'Create Meeting',
icon: 'fa-light fa-calendar-plus text-sm',
routerLink: ['meetings'],
routerLink: ['meetings/create'],
},
{
label: 'Create Committee',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,7 @@ <h4 class="text-sm font-semibold text-gray-900 mb-1">AI Agenda Generator</h4>
control="agenda"
id="meeting-agenda"
placeholder="Enter meeting agenda and key discussion points"
[rows]="6"
[autoResize]="true"
styleClass="w-full"
styleClass="w-full min-h-32"
data-testid="meeting-details-agenda-textarea"></lfx-textarea>
<p class="mt-1 text-xs text-gray-500">A clear agenda helps participants prepare and keeps discussions focused</p>
@if (form().get('agenda')?.errors?.['required'] && form().get('agenda')?.touched) {
Expand Down
Loading
Loading