Skip to content

Commit 47e48ec

Browse files
asithadeclaude
andauthored
feat(api): integrate NATS for project slug-to-ID resolution (#53)
* docs(chore): remove subagent system and simplify claude instructions Remove Claude Code subagent system configuration files and simplify CLAUDE.md documentation to reduce complexity and maintenance overhead. - Delete .claude/agents/angular-ui-expert.md - Delete .claude/agents/jira-project-manager.md - Remove subagent system section from CLAUDE.md - Simplify commit workflow documentation LFXV2-334 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * feat(ui): refactor meeting-create to meeting-manage with edit support - Rename meeting-create component to meeting-manage for both create/edit functionality - Add confirmation dialog for attachment deletion with proper refresh mechanism - Fix attachment saving in edit mode to properly add new attachments to existing meetings - Improve error handling: replace forkJoin all-or-nothing with individual attachment error handling - Add loading state signal for attachment deletion progress tracking - Extract reusable code to shared package for better maintainability: - Move constants (TOTAL_STEPS, DEFAULT_DURATION, etc.) to shared/constants/meeting.ts - Create validators directory with meeting-specific form validators - Add date/time utilities (combineDateTime, formatTo12Hour, etc.) to shared/utils - Refactor component to use extracted utilities, reducing code duplication by 700+ lines - Add Angular forms as peer dependency to shared package for validators - Update routing to support both /meetings/create and /meetings/:id/edit paths - Implement loading spinner for delete attachment button Addresses LFXV2-286: Meeting create/edit functionality consolidation Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix: update shared package deps Signed-off-by: Asitha de Silva <[email protected]> * fix(shared): correct MS_IN_DAY calculation and add comprehensive JSDoc - Fix critical bug in MS_IN_DAY constant calculation - Was incorrectly calculating milliseconds in a week (604,800,000) - Now correctly calculates milliseconds in a day (86,400,000) - Removed erroneous DAYS_IN_WEEK multiplication - Add comprehensive JSDoc documentation to all meeting constants - Time calculation constants with mathematical explanations - Meeting configuration defaults with usage examples - Form validation and navigation constants - Access control and recurrence mapping enums - Organize constants into logical sections with clear headers - Validate all time arithmetic is mathematically correct - Ensure date operations (like week addition) now work properly This fixes potential date calculation bugs in meeting recurrence logic. Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * feat(shared): standardize file naming and add comprehensive JSDoc - Standardize file naming with consistent suffixes (.constants, .interface, .enum, .validators, .utils) - Add comprehensive JSDoc documentation to all constants, enums, and interfaces - Fix critical MS_IN_DAY calculation bug (was calculating weeks instead of days) - Consolidate date and timezone utilities into date-time.utils.ts - Update all import/export statements to maintain compatibility - Improve code maintainability with self-documenting interfaces Generated with [Claude Code](https://claude.ai/code) LFXV2-286 Signed-off-by: Asitha de Silva <[email protected]> * fix(meeting): implement timezone-aware datetime handling - Add date-fns-tz dependency for proper timezone support - Update combineDateTime to accept timezone parameter and use fromZonedTime for UTC conversion - Replace brittle toLocaleString/new Date parsing with timezone-aware utility functions - Add timezone utility functions: compareDateTimesInTimezone, getCurrentTimeInTimezone, isDateTimeInFutureForTimezone - Update futureDateTimeValidator to use proper timezone handling - Remove duplicate validator and utility functions from meeting form component - Ensure meeting scheduling works correctly across different timezones LFXV2-286 Signed-off-by: Asitha de Silva <[email protected]> * fix(validation): prevent NaN duration and improve form validation - Replace ad-hoc custom duration validation with shared customDurationValidator - Add explicit time format validation using timeFormatValidator - Add topic and agenda content validators for better input validation - Add runtime safety check to prevent NaN duration from bypassing validation - Ensure consistent validation patterns across all meeting form components - Fix dependency management by removing duplicates from app package.json LFXV2-286 Signed-off-by: Asitha de Silva <[email protected]> * feat(api): integrate NATS for project slug-to-ID resolution - Add NATS.js SDK dependency for cluster messaging - Implement lazy-loading NATS service with request/reply pattern - Create consolidated getProjectBySlug method using NATS resolution - Add NATS interfaces and configuration for lfx.projects-api.slug_to_uid subject - Update project routes to use NATS for slug resolution - Configure cluster DNS connection to lfx-platform-nats.lfx.svc.cluster.local:4222 - Add graceful shutdown handling for NATS connections Resolves LFXV2-337 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Asitha de Silva <[email protected]> * refactor(api): improve NATS service architecture and thread safety - Extract hardcoded NATS URL to configuration constants in nats.config.ts - Implement thread-safe connection management with promise-based synchronization - Fix NatsService instantiation by using dependency injection instead of per-request instances - Add proper connection pooling and prevent race conditions during concurrent requests - Use configuration constants for timeouts and server URLs Performance improvements: - Reuse single NatsService instance across all slug lookups - Prevent multiple concurrent connection attempts with connectionPromise - Centralized configuration management for better maintainability Resolves architecture feedback on LFXV2-337 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Asitha de Silva <[email protected]> * feat(test): add playwright API mocking for NATS-dependent endpoints - Create organized mock data structure for projects in e2e/fixtures/mock-data/ - Add ApiMockHelper class to intercept /api/projects/:slug endpoint - Update project-dashboard.spec.ts and project-dashboard-robust.spec.ts to use mocks - Mock data includes ASWF, CNCF, and Kubernetes projects for testing - Prevents test failures when NATS server is unavailable in test environment 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix(api): correct project query parameter from uid to tags - Update getProjectById to use tags parameter for project ID lookup - Ensures proper project resolution after NATS slug-to-ID conversion 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * refactor(api): implement self-contained dependency injection pattern Controllers and services now initialize their own dependencies internally for cleaner architecture and simplified instantiation. 🤖 Generated with [Claude Code](https://claude.ai/code) LFXV2-337 Signed-off-by: Asitha de Silva <[email protected]> * refactor(api): move NATS config to shared package Move NATS configuration constants from app-specific config to shared package for better reusability and centralized configuration management. 🤖 Generated with [Claude Code](https://claude.ai/code) LFXV2-337 Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 9fcea72 commit 47e48ec

File tree

24 files changed

+703
-253
lines changed

24 files changed

+703
-253
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
22
"cSpell.words": [
33
"animateonscroll",
4+
"aswf",
45
"AUTHELIA",
6+
"autojoin",
57
"autorestart",
68
"cloudops",
79
"confirmdialog",

apps/lfx-pcc/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ SUPABASE_URL=https://your-project.supabase.co
2121
POSTGRES_API_KEY=your-supabase-anon-key
2222
SUPABASE_STORAGE_BUCKET=your-supabase-bucket-name
2323

24+
# NATS Configuration
25+
# Internal k8s service DNS for NATS cluster
26+
NATS_URL=nats://lfx-platform-nats.lfx.svc.cluster.local:4222
27+
2428
# E2E Test Configuration (Optional)
2529
# Test user credentials for automated testing
2630
TEST_USERNAME=your-test-username
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
/**
5+
* Central exports for all mock data
6+
* Provides easy access to project mock data for Playwright tests
7+
*/
8+
9+
export * from './projects.mock';
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Project } from '@lfx-pcc/shared/interfaces';
5+
6+
/**
7+
* Mock project data for Playwright tests
8+
* Organized by project slug for easy lookup
9+
*/
10+
export const mockProjects: Record<string, Project> = {
11+
aswf: {
12+
uid: 'a09f1234-f567-4abc-b890-1234567890ab',
13+
slug: 'aswf',
14+
name: 'Academy Software Foundation (ASWF)',
15+
description:
16+
"The mission of the Academy Software Foundation (ASWF) is to increase the quality and quantity of contributions to the content creation industry's open source software base; to provide a neutral forum to coordinate cross-project efforts; to provide a common build and test infrastructure; and to provide individuals and organizations a clear path to participation in advancing our open source ecosystem.",
17+
public: true,
18+
parent_uid: '',
19+
stage: 'graduated',
20+
category: 'foundation',
21+
funding_model: ['member-funded'],
22+
charter_url: 'https://github.com/AcademySoftwareFoundation/tac/blob/main/charter.md',
23+
legal_entity_type: 'directed-fund',
24+
legal_entity_name: 'Academy Software Foundation',
25+
legal_parent_uid: '',
26+
autojoin_enabled: false,
27+
formation_date: '2018-08-10T00:00:00Z',
28+
logo_url: 'https://artwork.aswf.io/projects/aswf/horizontal/color/aswf-horizontal-color.png',
29+
repository_url: 'https://github.com/AcademySoftwareFoundation',
30+
website_url: 'https://www.aswf.io/',
31+
created_at: '2018-08-10T00:00:00Z',
32+
updated_at: new Date().toISOString(),
33+
committees_count: 5,
34+
meetings_count: 23,
35+
mailing_list_count: 8,
36+
},
37+
cncf: {
38+
uid: 'b09f1234-f567-4abc-b890-1234567890bc',
39+
slug: 'cncf',
40+
name: 'Cloud Native Computing Foundation',
41+
description:
42+
"The Cloud Native Computing Foundation (CNCF) hosts critical components of the global technology infrastructure. CNCF brings together the world's top developers, end users, and vendors and runs the largest open source developer conferences.",
43+
public: true,
44+
parent_uid: '',
45+
stage: 'graduated',
46+
category: 'foundation',
47+
funding_model: ['member-funded'],
48+
charter_url: 'https://github.com/cncf/foundation/blob/master/charter.md',
49+
legal_entity_type: 'directed-fund',
50+
legal_entity_name: 'Cloud Native Computing Foundation',
51+
legal_parent_uid: '',
52+
autojoin_enabled: false,
53+
formation_date: '2015-12-11T00:00:00Z',
54+
logo_url: 'https://landscape.cncf.io/logos/cncf-color.svg',
55+
repository_url: 'https://github.com/cncf',
56+
website_url: 'https://www.cncf.io/',
57+
created_at: '2015-12-11T00:00:00Z',
58+
updated_at: new Date().toISOString(),
59+
committees_count: 12,
60+
meetings_count: 156,
61+
mailing_list_count: 25,
62+
},
63+
kubernetes: {
64+
uid: 'c09f1234-f567-4abc-b890-1234567890cd',
65+
slug: 'kubernetes',
66+
name: 'Kubernetes',
67+
description: 'Kubernetes is an open-source system for automating deployment, scaling, and management of containerized applications.',
68+
public: true,
69+
parent_uid: 'b09f1234-f567-4abc-b890-1234567890bc', // CNCF parent
70+
stage: 'graduated',
71+
category: 'project',
72+
funding_model: ['foundation-funded'],
73+
charter_url: 'https://github.com/kubernetes/community/blob/master/governance.md',
74+
legal_entity_type: 'project',
75+
legal_entity_name: 'Kubernetes',
76+
legal_parent_uid: 'b09f1234-f567-4abc-b890-1234567890bc',
77+
autojoin_enabled: true,
78+
formation_date: '2014-06-07T00:00:00Z',
79+
logo_url: 'https://kubernetes.io/images/kubernetes-horizontal-color.png',
80+
repository_url: 'https://github.com/kubernetes/kubernetes',
81+
website_url: 'https://kubernetes.io/',
82+
created_at: '2014-06-07T00:00:00Z',
83+
updated_at: new Date().toISOString(),
84+
committees_count: 8,
85+
meetings_count: 89,
86+
mailing_list_count: 15,
87+
},
88+
};
89+
90+
/**
91+
* Get mock project by slug
92+
* @param slug - Project slug to look up
93+
* @returns Project object or undefined if not found
94+
*/
95+
export function getMockProject(slug: string): Project | undefined {
96+
return mockProjects[slug];
97+
}
98+
99+
/**
100+
* Get all mock projects as an array
101+
* @returns Array of all mock projects
102+
*/
103+
export function getAllMockProjects(): Project[] {
104+
return Object.values(mockProjects);
105+
}
106+
107+
/**
108+
* Search mock projects by name or description
109+
* @param query - Search query string
110+
* @returns Array of matching projects
111+
*/
112+
export function searchMockProjects(query: string): Project[] {
113+
const searchTerm = query.toLowerCase();
114+
return getAllMockProjects().filter(
115+
(project) =>
116+
project.name.toLowerCase().includes(searchTerm) ||
117+
project.description.toLowerCase().includes(searchTerm) ||
118+
project.slug.toLowerCase().includes(searchTerm)
119+
);
120+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Page } from '@playwright/test';
5+
6+
import { getMockProject } from '../fixtures/mock-data';
7+
8+
/**
9+
* Helper class for mocking the project slug endpoint in Playwright tests
10+
* Only handles /api/projects/:slug endpoint that depends on NATS
11+
*/
12+
export class ApiMockHelper {
13+
/**
14+
* Setup mock for the project slug endpoint only
15+
* @param page - Playwright page instance
16+
*/
17+
static async setupProjectSlugMock(page: Page): Promise<void> {
18+
// Mock individual project by slug endpoint (/api/projects/:slug)
19+
await page.route('**/api/projects/*', async (route) => {
20+
const url = route.request().url();
21+
22+
// Skip other endpoints - only handle direct slug requests
23+
if (url.includes('/search') || url.includes('/recent-activity')) {
24+
await route.continue();
25+
return;
26+
}
27+
28+
const pathSegments = url.split('/');
29+
const slug = pathSegments[pathSegments.length - 1].split('?')[0]; // Remove query params
30+
31+
console.log(`[Mock] Intercepting project request for slug: "${slug}"`);
32+
33+
const project = getMockProject(slug);
34+
35+
if (!project) {
36+
await route.fulfill({
37+
status: 404,
38+
contentType: 'application/json',
39+
body: JSON.stringify({
40+
error: 'Project not found',
41+
code: 'PROJECT_NOT_FOUND',
42+
}),
43+
});
44+
return;
45+
}
46+
47+
await route.fulfill({
48+
status: 200,
49+
contentType: 'application/json',
50+
body: JSON.stringify(project),
51+
});
52+
});
53+
}
54+
}

apps/lfx-pcc/e2e/homepage.spec.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

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

6+
import { ApiMockHelper } from './helpers/api-mock.helper';
7+
68
test.describe('Homepage', () => {
79
test.beforeEach(async ({ page }) => {
810
await page.goto('/', { waitUntil: 'domcontentloaded' });
@@ -112,6 +114,9 @@ test.describe('Homepage', () => {
112114
});
113115

114116
test('should filter projects when searching', async ({ page }) => {
117+
// Setup API mocks before navigation
118+
await ApiMockHelper.setupProjectSlugMock(page);
119+
115120
// Wait for project cards to appear
116121
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });
117122

@@ -144,6 +149,9 @@ test.describe('Homepage', () => {
144149
});
145150

146151
test('should clear search and show all projects', async ({ page }) => {
152+
// Setup API mocks before navigation
153+
await ApiMockHelper.setupProjectSlugMock(page);
154+
147155
// Wait for project cards to appear
148156
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });
149157

@@ -167,6 +175,9 @@ test.describe('Homepage', () => {
167175
});
168176

169177
test('should navigate to project detail when clicking a project card', async ({ page }) => {
178+
// Setup API mocks before navigation
179+
await ApiMockHelper.setupProjectSlugMock(page);
180+
170181
// Wait for project cards to appear
171182
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });
172183

@@ -176,17 +187,14 @@ test.describe('Homepage', () => {
176187
.filter({ has: page.getByTestId('project-title').filter({ hasText: 'Cloud Native Computing Foundation' }) });
177188
await expect(cncfCard).toBeVisible();
178189

179-
// Get the project name to verify navigation
180-
const projectName = await cncfCard.getByTestId('project-title').innerText();
181-
182190
// Click the project card
183191
await cncfCard.click();
184192

185193
// Wait for navigation by checking URL change
186194
await expect(page).toHaveURL(/\/project\/[\w-]+$/, { timeout: 10000 });
187195

188196
// Verify project detail page elements - use heading that contains the project name
189-
await expect(page.getByRole('heading', { level: 1 }).filter({ hasText: projectName })).toBeVisible();
197+
await expect(page.getByRole('heading', { level: 1 }).filter({ hasText: 'Cloud Native Computing Foundation' })).toBeVisible();
190198
await expect(page.getByRole('link', { name: 'All Projects' })).toBeVisible();
191199
await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
192200
await expect(page.getByRole('link', { name: 'Meetings' })).toBeVisible();

apps/lfx-pcc/e2e/project-dashboard-robust.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33

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

6+
import { ApiMockHelper } from './helpers/api-mock.helper';
7+
68
test.describe('Project Dashboard - Robust Tests', () => {
79
test.beforeEach(async ({ page }) => {
10+
// Setup API mocks before navigation
11+
await ApiMockHelper.setupProjectSlugMock(page);
12+
813
// Navigate to homepage and search for a project
914
await page.goto('/', { waitUntil: 'domcontentloaded' });
1015

apps/lfx-pcc/e2e/project-dashboard.spec.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33

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

6+
import { ApiMockHelper } from './helpers/api-mock.helper';
7+
68
test.describe('Project Dashboard', () => {
79
test.beforeEach(async ({ page }) => {
10+
// Setup API mocks before navigation
11+
await ApiMockHelper.setupProjectSlugMock(page);
12+
813
// Navigate to homepage and search for a project
914
await page.goto('/', { waitUntil: 'domcontentloaded' });
1015

@@ -68,11 +73,12 @@ test.describe('Project Dashboard', () => {
6873
});
6974

7075
test('should display all navigation tabs', async ({ page }) => {
71-
await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
72-
await expect(page.getByRole('link', { name: 'Meetings' })).toBeVisible();
73-
await expect(page.getByRole('link', { name: 'Committees' })).toBeVisible();
74-
await expect(page.getByRole('link', { name: 'Mailing Lists' })).toBeVisible();
75-
await expect(page.getByRole('link', { name: 'Settings' })).toBeVisible();
76+
console.log(await page.getByTestId('menu-item').allInnerTexts());
77+
await expect(page.getByTestId('menu-item').filter({ hasText: 'Dashboard' })).toBeVisible();
78+
await expect(page.getByTestId('menu-item').filter({ hasText: 'Meetings' })).toBeVisible();
79+
await expect(page.getByTestId('menu-item').filter({ hasText: 'Committees' })).toBeVisible();
80+
await expect(page.getByTestId('menu-item').filter({ hasText: 'Mailing Lists' })).toBeVisible();
81+
await expect(page.getByTestId('menu-item').filter({ hasText: 'Settings' })).toBeVisible();
7682
});
7783
});
7884

apps/lfx-pcc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"dotenv": "^17.2.1",
4040
"express": "^4.18.2",
4141
"express-openid-connect": "^2.19.2",
42+
"nats": "^2.29.3",
4243
"ngx-cookie-service-ssr": "^19.1.2",
4344
"pino-http": "^10.5.0",
4445
"primeng": "^19.1.4",

apps/lfx-pcc/src/app/layouts/project-layout/project-layout.component.html

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
4343
<div class="flex items-center gap-8 mb-8 flex-wrap">
4444
@for (metric of metrics(); track metric.label) {
4545
<div class="flex flex-col">
46-
<div class="flex items-center gap-2 mb-1">
46+
<div class="flex items-center gap-2 mb-1" data-testid="metric-label">
4747
<i [class]="metric.icon" class="text-sm"></i>
4848
<span class="text-sm text-gray-600">{{ metric.label }}</span>
4949
</div>
50-
<div class="text-2xl font-normal text-gray-900 font-display">{{ metric.value }}</div>
50+
<div class="text-2xl font-normal text-gray-900 font-display" data-testid="metric-value">{{ metric.value }}</div>
5151
</div>
5252
}
5353
</div>
@@ -62,6 +62,7 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
6262
[routerLink]="menu.routerLink"
6363
class="pill"
6464
routerLinkActive="bg-blue-50 text-blue-600 border-0"
65+
data-testid="menu-item"
6566
[routerLinkActiveOptions]="menu.routerLinkActiveOptions">
6667
@if (menu.icon) {
6768
<i [class]="menu.icon"></i>
@@ -74,9 +75,10 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
7475
routerLink="/project/{{ projectSlug() }}/settings"
7576
class="pill"
7677
routerLinkActive="bg-blue-50 text-blue-600 border-0 block md:hidden"
78+
data-testid="mobile-menu-item"
7779
[routerLinkActiveOptions]="{ exact: true }">
7880
<i class="fa-light fa-gear"></i>
79-
Settings
81+
<span>Settings</span>
8082
</a>
8183
</div>
8284
</div>
@@ -85,9 +87,10 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
8587
routerLink="/project/{{ projectSlug() }}/settings"
8688
class="pill"
8789
routerLinkActive="bg-blue-50 text-blue-600 border-0"
90+
data-testid="menu-item"
8891
[routerLinkActiveOptions]="{ exact: true }">
8992
<i class="fa-light fa-gear"></i>
90-
Settings
93+
<span>Settings</span>
9194
</a>
9295
</div>
9396
</div>

0 commit comments

Comments
 (0)