Skip to content

Conversation

@asithade
Copy link
Contributor

Summary

Comprehensive rename of the application from "LFX PCC" (Project Control Center) and "LFX Projects" to "LFX One" across the entire codebase for improved branding and consistency.

Changes Made

  • 🏗️ Application Structure: Renamed folder from apps/lfx-pcc/ to apps/lfx-one/
  • 📦 Package References: Updated shared package from @lfx-pcc/shared to @lfx-one/shared
  • 🎨 User-Facing Text: Updated all UI text, page titles, and tooltips
  • 📚 Documentation: Updated all architecture documentation, READMEs, and guides
  • 🔧 CI/CD: Updated Docker image labels and workflow descriptions
  • ⚙️ Configuration: Updated PM2 process names and script references

Files Affected

  • 283 files changed across the entire codebase
  • Frontend components, backend services, documentation, and configuration files
  • All references to "LFX PCC", "LFX Projects", and "Project Control Center" updated to "LFX One"

Test plan

  • All lint checks pass
  • Code formatting verified with Prettier
  • Build process validated successfully
  • Type checking passes
  • E2E tests should be run to verify page title changes
  • Manual testing of UI components to verify text updates

Technical Impact

  • Package manager references updated in package.json files
  • Build and deployment scripts updated
  • E2E test expectations updated for new page titles
  • Helm chart documentation updated

JIRA: LFXV2-489

🤖 Generated with Claude Code

- Rename apps/lfx-pcc/ to apps/lfx-one/ for cleaner branding
- Update package references from @lfx-pcc/shared to @lfx-one/shared
- Update all user-facing text from "LFX Projects"/"LFX PCC" to "LFX One"
- Update page titles, tooltips, and UI components
- Update comprehensive documentation across all architecture docs
- Update CI/CD workflows and Docker image labels
- Update Helm chart documentation and configuration
- Update PM2 process management configuration

LFXV2-489

🤖 Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Asitha de Silva <[email protected]>
Copilot AI review requested due to automatic review settings September 15, 2025 23:52
@asithade asithade requested a review from jordane as a code owner September 15, 2025 23:52
@coderabbitai
Copy link

coderabbitai bot commented Sep 15, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Repository-wide rebrand from "LFX PCC" to "LFX One": package and workspace names, TypeScript path aliases, app directories, import paths, Docker/PM2 identifiers, CI/E2E workflow paths and image labels, UI text/titles, and documentation updated. No functional runtime logic changes introduced.

Changes

Cohort / File(s) Change Summary
CI Docker metadata
.github/workflows/docker-build-*.yml
Updated OCI image labels: org.opencontainers.image.title and org.opencontainers.image.description to reference “LFX One UI”.
CI E2E workflow path
.github/workflows/e2e-tests.yml
E2E steps working-directory changed from apps/lfx-pccapps/lfx-one.
Docker / runtime scripts
Dockerfile, apps/lfx-one/ecosystem.config.js, package.json
Dockerfile now copies/starts apps/lfx-one; PM2 app name/script and top-level npm scripts updated from lfx-pcclfx-one.
App config & aliases
apps/lfx-one/angular.json, apps/lfx-one/package.json, apps/lfx-one/tsconfig.json, apps/lfx-one/tailwind.config.js, apps/lfx-one/src/index.html
Project key, output paths, package name, and TS path aliases renamed from lfx-pcc/@lfx-pcc/sharedlfx-one/@lfx-one/shared; Tailwind imports and page title updated.
Shared package rename
packages/shared/package.json
Package name changed from @lfx-pcc/shared@lfx-one/shared.
Frontend imports & UI text
apps/lfx-one/src/app/... (components, modules, pipes, services, templates)
Systematic import path updates: @lfx-pcc/shared/...@lfx-one/shared/...; small UI label/text updates (e.g., header "Projects" → "One"); no logic changes.
Server imports
apps/lfx-one/src/server/... (controllers, routes, services, middleware, utils, errors)
Systematic import path updates: @lfx-pcc/shared/...@lfx-one/shared/...; startup paths adjusted to dist/lfx-one/....
E2E tests & fixtures
apps/lfx-one/e2e/*.spec.ts, apps/lfx-one/e2e/fixtures/*
Test expectations updated to "LFX One" and imports switched to @lfx-one/shared/*.
Docs, READMEs & charts
CLAUDE.md, CONTRIBUTING.md, README.md, docs/**, charts/lfx-v2-ui/README.md, apps/lfx-one/README.md
Extensive documentation and example path/name updates from LFX PCC → LFX One; build/deploy examples and commands updated accordingly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ❓ Inconclusive Most changes are rename-related and in-scope, however I observed additional documentation additions (CLAUDE.md includes new developer guidelines beyond a simple rename) and a few minor non-functional edits (import reorderings and an added RxJS operator list) that are not explicitly requested by LFXV2-489 and whose intent is not documented in the PR. These items are low-risk but technically fall outside the strict rename objective, so I cannot conclusively state the PR is strictly rename-only. Please confirm whether the CLAUDE.md guideline additions and the minor import/operator edits were intended to be part of this rename PR; if not, revert or move them to a separate PR and ensure E2E/CI pass before merging.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "feat(ui): rename application from LFX PCC to LFX One" is a concise, single-sentence description that directly summarizes the primary change (a repo-wide rename) and accurately reflects the changeset; it is specific and clear for reviewers scanning history. The title avoids extraneous noise and correctly indicates scope and intent.
Linked Issues Check ✅ Passed The code-level objectives from the linked issue are implemented: repository and package renames with tsconfig path updates; import refactors across frontend and backend plus package.json, PM2, Docker and CI/workflow updates; and user-facing text, E2E expectation, and documentation updates—these grouped changes satisfy the linked issue's coding requirements. The diff shows apps/lfx-one paths, the @lfx-one/shared package rename, Docker/CI label edits, and updated test assertions, which together meet the primary objectives. E2E and manual verification remain pending in the PR test plan and should be executed before merge to confirm runtime behavior. This assessment is made with respect to the linked issue LFXV2-489.
Description Check ✅ Passed The PR description is on-topic and aligns with the changeset, providing a clear summary of what was renamed, categories of changes (structure, package refs, UI text, docs, CI/CD, config), a test plan, and a JIRA reference, so it meaningfully describes the changes. The level of detail is sufficient for reviewers to understand intent even though some tests are still listed as pending. This is not unrelated or off-topic content.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/LFXV2-489

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR renames the application from "LFX PCC" (Project Control Center) to "LFX One" across the entire codebase for improved branding and consistency. The changes include updating package names, application references, and user-facing text while maintaining all functionality.

  • Renamed folder structure from apps/lfx-pcc/ to apps/lfx-one/
  • Updated shared package references from @lfx-pcc/shared to @lfx-one/shared
  • Changed all user-facing text, documentation, and configuration files

Reviewed Changes

Copilot reviewed 117 out of 283 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/shared/package.json Updated package name to @lfx-one/shared
package.json Renamed root package and PM2 script references
Multiple TypeScript files Updated import statements to use new shared package name
Documentation files Updated references from LFX PCC to LFX One
Configuration files Updated Angular config, Docker files, and CI/CD references
E2E tests Updated page title expectations

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
apps/lfx-one/src/server/services/ai.service.ts (3)

13-23: Remove hardcoded API key fallback; enforce env var.
Defaulting to a fake key risks accidental prod usage and weakens secrets handling.

Apply:

-  private readonly aiKey = process.env['AI_API_KEY'] || 'sk-proj-1234567890';
+  private readonly aiKey: string;
...
-    this.aiProxyUrl = process.env['AI_PROXY_URL'] || 'https://api.openai.com/v1/chat/completions';
-    if (!this.aiProxyUrl) {
-      throw new Error('AI_PROXY_URL environment variable is required');
-    }
-
-    if (!this.aiKey) {
-      throw new Error('AI_API_KEY environment variable is required');
-    }
+    this.aiProxyUrl = this.requireEnv('AI_PROXY_URL');
+    this.aiKey = this.requireEnv('AI_API_KEY');
+  }
+
+  private requireEnv(name: string): string {
+    const v = process.env[name];
+    if (!v) throw new Error(`${name} environment variable is required`);
+    return v;
   }

123-139: Add request timeout and abort; set Accept header.
External call without timeout can hang request threads.

Apply:

   private async makeAiRequest(request: OpenAIChatRequest): Promise<OpenAIChatResponse> {
-    const response = await fetch(this.aiProxyUrl, {
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 30000); // 30s
+    const response = await fetch(this.aiProxyUrl, {
       method: 'POST',
       headers: {
         ['Content-Type']: 'application/json',
+        ['Accept']: 'application/json',
         ['Authorization']: `Bearer ${this.aiKey}`,
       },
-      body: JSON.stringify(request),
+      body: JSON.stringify(request),
+      signal: controller.signal,
     });
+    clearTimeout(timeout);

172-175: Avoid logging raw model output (possible sensitive content).
Trim or remove content from logs to prevent PII leakage.

Apply:

-      serverLogger.warn('Failed to parse JSON response, falling back to text extraction', {
-        content: content.substring(0, 100),
-        error: parseError,
-      });
+      serverLogger.warn('Failed to parse JSON response; falling back to text extraction', {
+        content_preview_len: Math.min(content.length, 100),
+        error: (parseError as Error).message,
+      });
apps/lfx-one/src/server/helpers/logger.ts (1)

17-26: Sanitize metadata before logging (sanitize() is never applied).
Current logs spread raw metadata; sensitive keys may leak. Apply sanitize() in all helpers before spreading metadata.

   public static start(req: Request, operation: string, metadata: Record<string, any> = {}): number {
     const startTime = Date.now();

-    req.log.info(
+    const meta = Logger.sanitize(metadata);
+    req.log.info(
       {
         operation,
-        ...metadata,
+        ...meta,
         request_id: req.id,
         user_agent: req.get('User-Agent'),
         ip_address: req.ip,
       },
       `Starting ${operation.replace(/_/g, ' ')}`
     );
   public static success(req: Request, operation: string, startTime: number, metadata: Record<string, any> = {}): void {
     const duration = Date.now() - startTime;
-    req.log.info(
+    const meta = Logger.sanitize(metadata);
+    req.log.info(
       {
         operation,
         duration,
-        status_code: metadata['status_code'] || 200,
-        ...metadata,
+        status_code: meta['status_code'] ?? 200,
+        ...meta,
         request_id: req.id,
       },
       `Successfully completed ${operation.replace(/_/g, ' ')}`
     );
   public static error(req: Request, operation: string, startTime: number, error: unknown, metadata: Record<string, any> = {}): void {
     const duration = Date.now() - startTime;
-    req.log.error(
+    const meta = Logger.sanitize(metadata);
+    req.log.error(
       {
         operation,
         duration,
         error: error instanceof Error ? error.message : String(error),
-        ...metadata,
+        ...meta,
         request_id: req.id,
       },
       `Failed to ${operation.replace(/_/g, ' ')}`
     );
   public static validation(req: Request, operation: string, validationErrors: any[], metadata: Record<string, any> = {}): void {
-    req.log.warn(
+    const meta = Logger.sanitize(metadata);
+    req.log.warn(
       {
         operation,
         validation_errors: validationErrors,
         status_code: 400,
-        ...metadata,
+        ...meta,
         request_id: req.id,
       },
       `Validation failed for ${operation.replace(/_/g, ' ')}`
     );
   public static etag(req: Request, operation: string, resourceType: string, resourceId: string, etag?: string, metadata: Record<string, any> = {}): void {
-    req.log.info(
+    const meta = Logger.sanitize(metadata);
+    req.log.info(
       {
         operation,
         resource_type: resourceType,
         resource_id: resourceId,
         etag,
-        ...metadata,
+        ...meta,
         request_id: req.id,
       },
       `ETag operation: ${operation.replace(/_/g, ' ')}`
     );
   public static warning(req: Request, operation: string, message: string, metadata: Record<string, any> = {}): void {
-    req.log.warn(
+    const meta = Logger.sanitize(metadata);
+    req.log.warn(
       {
         operation,
         warning_message: message,
-        ...metadata,
+        ...meta,
         request_id: req.id,
       },
       `Warning during ${operation.replace(/_/g, ' ')}: ${message}`
     );

Also applies to: 37-46, 55-64, 71-80, 87-97, 104-112, 118-128

apps/lfx-one/src/server/services/api-client.service.ts (1)

38-44: Update User-Agent to LFX One.
Leftover brand string in a public header.

       Accept: 'application/json',
       ['Content-Type']: 'application/json',
-      ['User-Agent']: 'LFX-PCC-Server/1.0',
+      ['User-Agent']: 'LFX-One-Server/1.0',
apps/lfx-one/src/app/modules/project/meetings/components/meeting-recurrence-pattern/meeting-recurrence-pattern.component.ts (1)

93-109: Guard against undefined startDate to prevent runtime errors.

Several paths call .getDate()/.getDay() on a possibly undefined start date (common during initial form render), which will throw.

Apply this diff:

@@
-  public onMonthlyTypeChange(monthlyType: string): void {
+  public onMonthlyTypeChange(monthlyType: string): void {
     const recurrenceForm = this.recurrenceForm();
     if (!recurrenceForm) return;
 
     if (monthlyType === 'dayOfMonth') {
       recurrenceForm.patchValue({
-        monthly_day: this.startDate().getDate(),
+        monthly_day: (this.startDate()?.getDate() ?? new Date().getDate()),
         monthly_week: null,
         monthly_week_day: null,
       });
     } else {
       // dayOfWeek
-      const startDate = this.startDate();
+      const startDate = this.startDate() ?? new Date();
       const { weekOfMonth } = getWeekOfMonth(startDate);
       recurrenceForm.patchValue({
         monthly_day: null,
         monthly_week: weekOfMonth,
         monthly_week_day: startDate.getDay() + 1, // Convert 0-6 to 1-7
       });
     }
   }
@@
   recurrenceForm.patchValue({
     type: type,
     // Clear pattern-specific fields when changing pattern type
     weekly_days: patternType === 'weekly' ? this.getDefaultWeeklyDays() : null,
-    monthly_day: patternType === 'monthly' ? this.startDate().getDate() : null,
+    monthly_day: patternType === 'monthly' ? (this.startDate()?.getDate() ?? new Date().getDate()) : null,
     monthly_week: null,
     monthly_week_day: null,
   });
@@
-  private getDefaultWeeklyDays(): string {
-    // Default to the current start date's day of week
-    const startDate = this.startDate();
-    return String(startDate.getDay() + 1); // Convert 0-6 to 1-7
-  }
+  private getDefaultWeeklyDays(): string {
+    // Default to start date's DOW, or today if start date not yet set
+    const base = this.startDate() ?? new Date();
+    return String(base.getDay() + 1); // Convert 0-6 to 1-7
+  }

Also applies to: 174-182, 296-300

apps/lfx-one/src/server/routes/permissions.route.ts (1)

79-83: Avoid logging PII (names/emails) in structured logs.

userData includes email and names; emitting it to logs is a privacy risk. Log only non-PII metadata.

Apply this diff:

-    req.log.info({ projectId, userData }, 'Creating user with permissions');
+    req.log.info(
+      {
+        projectId,
+        permission_scope: userData.permission_scope,
+        permission_level: userData.permission_level,
+        committee_ids_count: userData.committee_ids?.length ?? 0,
+      },
+      'Creating user with permissions'
+    );
apps/lfx-one/src/app/modules/meeting/meeting-join/meeting-join.component.ts (2)

113-143: Bug: unsafe signal initialization and null access; possible runtime error when meeting is undefined.

  • toSignal has no initialValue; yet code accesses this.meeting().uid (and others) without null checks → crash before data arrives.
  • Error path returns an empty object then spreads res.meeting, which can be undefined → TypeError on spread.

Fix by making meeting nullable, providing an initialValue, guarding accesses, and avoiding spread on possibly undefined.

Apply this diff:

-  public meeting: Signal<Meeting & { project: Project }>;
+  public meeting: Signal<(Meeting & { project: Project }) | null>;
@@
-  public returnTo: Signal<string | undefined>;
+  public returnTo: Signal<string | undefined>;
@@
-  private initializeMeeting() {
-    return toSignal<Meeting & { project: Project }>(
+  private initializeMeeting() {
+    return toSignal<(Meeting & { project: Project }) | null>(
       combineLatest([this.activatedRoute.paramMap, this.activatedRoute.queryParamMap]).pipe(
         switchMap(([params, queryParams]) => {
           const meetingId = params.get('id');
           this.password.set(queryParams.get('password'));
           if (meetingId) {
             return this.meetingService.getPublicMeeting(meetingId, this.password()).pipe(
               catchError((error) => {
                 // If 404, navigate to not found page
                 if ([404, 403, 400].includes(error.status)) {
                   this.router.navigate(['/meetings/not-found']);
-                  return of({} as { meeting: Meeting; project: Project });
+                  return of(null);
                 }
                 // Re-throw other errors
                 throw error;
               })
             );
           }
 
           // If no meeting ID, redirect to not found
           this.router.navigate(['/meetings/not-found']);
-          return of({} as { meeting: Meeting; project: Project });
+          return of(null);
         }),
-        map((res) => ({ ...res.meeting, project: res.project })),
-        tap((res) => {
-          this.project.set(res.project);
+        map((res) => (res && res.meeting && res.project ? ({ ...res.meeting, project: res.project }) : null)),
+        tap((res) => {
+          if (res?.project) this.project.set(res.project);
         })
       )
-    ) as Signal<Meeting & { project: Project }>;
+    , { initialValue: null });
   }
@@
   public onJoinMeeting(): void {
-    if (!this.canJoinMeeting()) {
+    if (!this.canJoinMeeting() || !this.meeting()) {
       this.messageService.add({
         severity: 'warn',
         summary: 'Meeting Not Available',
         detail: 'The meeting has not started yet.',
       });
       return;
     }
@@
-      .getPublicMeetingJoinUrl(this.meeting().uid, this.meeting().password, {
+      .getPublicMeetingJoinUrl(this.meeting()!.uid, this.meeting()!.password, {
         email: this.authenticated() ? this.user()?.email : this.joinForm.get('email')?.value,
       })
@@
-  private initializeReturnTo(): Signal<string | undefined> {
+  private initializeReturnTo(): Signal<string | undefined> {
     return computed(() => {
-      return `${environment.urls.home}/meetings/${this.meeting().uid}?password=${this.password()}`;
+      const m = this.meeting();
+      if (!m) return undefined;
+      const pw = this.password();
+      return pw ? `${environment.urls.home}/meetings/${m.uid}?password=${pw}` : `${environment.urls.home}/meetings/${m.uid}`;
     });
   }
@@
-  private initializeJoinUrlWithParams(): Signal<string | undefined> {
+  private initializeJoinUrlWithParams(): Signal<string | undefined> {
     return computed(() => {
-      const meeting = this.meeting();
+      const meeting = this.meeting();
       const joinUrl = meeting?.join_url;
 
       if (!joinUrl) {
         return undefined;
       }
 
       // Access form values to trigger reactivity
       const formValues = this.formValues();
       return this.buildJoinUrlWithParams(joinUrl, formValues);
     });
   }

Also applies to: 207-211, 84-111, 229-242


104-106: Security: prevent reverse‑tabnabbing when opening external windows.

Use noopener/noreferrer and clear opener.

Apply this diff:

-          window.open(joinUrlWithParams, '_blank');
+          const win = window.open(joinUrlWithParams, '_blank', 'noopener,noreferrer');
+          if (win) win.opener = null;
apps/lfx-one/src/app/modules/project/committees/components/committee-form/committee-form.component.ts (1)

216-219: Bug: self‑parent exclusion compares uid to id (mismatch)

Current committee detection uses id but list uses uid, so the item isn’t excluded and users can set a committee as its own parent.

Apply this diff:

-          const currentCommitteeId = this.committee()?.id;
-          const availableCommittees = currentCommitteeId ? topLevelCommittees.filter((committee) => committee.uid !== currentCommitteeId) : topLevelCommittees;
+          const currentCommitteeUid = this.committee()?.uid;
+          const availableCommittees = currentCommitteeUid
+            ? topLevelCommittees.filter((committee) => committee.uid !== currentCommitteeUid)
+            : topLevelCommittees;
apps/lfx-one/src/app/modules/project/settings/settings-dashboard/settings-dashboard.component.ts (1)

41-54: Bug: users never loads if project()?.uid is initially null.
The source for toSignal is chosen once in the constructor; later project updates don’t re-subscribe. Bind the stream to the project signal (and refresh) so it reacts when the project arrives/changes.

-  public constructor() {
-    // Initialize userPermissions signal from service
-    this.users = toSignal(
-      this.project()?.uid
-        ? this.refresh.pipe(
-            tap(() => this.loading.set(true)),
-            switchMap(() => this.permissionsService.getProjectPermissions(this.project()?.uid as string).pipe(tap(() => this.loading.set(false))))
-          )
-        : of([]),
-      {
-        initialValue: [],
-      }
-    );
-  }
+  public constructor() {
+    const project$ = toObservable(this.project);
+    this.users = toSignal(
+      combineLatest([project$, this.refresh]).pipe(
+        switchMap(([project]) => {
+          if (!project?.uid) return of([]);
+          this.loading.set(true);
+          return this.permissionsService.getProjectPermissions(project.uid).pipe(
+            tap(() => this.loading.set(false))
+          );
+        })
+      ),
+      { initialValue: [] }
+    );
+  }

Add required imports:

- import { toSignal } from '@angular/core/rxjs-interop';
+ import { toObservable, toSignal } from '@angular/core/rxjs-interop';
- import { BehaviorSubject, of, switchMap, take, tap } from 'rxjs';
+ import { BehaviorSubject, combineLatest, of, switchMap, take, tap } from 'rxjs';
🧹 Nitpick comments (66)
docs/architecture/testing/e2e-testing.md (1)

156-165: Replace remaining “Projects Self-Service” strings with “LFX One” in examples

Two example blocks still reference the old brand; update to avoid confusion and keep docs consistent with the rebrand.

-    await expect(page.getByText('Projects Self-Service')).toBeHidden();
+    await expect(page.getByText('LFX One')).toBeHidden();
...
-    await expect(page.getByText('Projects Self-Service')).toBeVisible();
+    await expect(page.getByText('LFX One')).toBeVisible();
-  await expect(page.getByText('Projects Self-Service')).toBeHidden();
+  await expect(page.getByText('LFX One')).toBeHidden();
...
-  await expect(page.getByText('Projects Self-Service')).toBeVisible();
+  await expect(page.getByText('LFX One')).toBeVisible();

Also applies to: 218-231

apps/lfx-one/src/app/modules/project/meetings/components/meeting-platform-features/meeting-platform-features.component.ts (3)

11-11: Prefer LFX wrapper over direct PrimeNG Tooltip import (policy carry‑over).

Per past guidance we learned for this codebase, use LFX wrapper components instead of importing PrimeNG directly. If an LFX tooltip wrapper exists, switch to it for consistency and theming; otherwise ignore.

Note: Referencing retrieved learning from earlier reviews; please confirm whether this convention still applies post‑rename.


29-36: Externalize “(Coming Soon)” for i18n.

Avoid hard‑coded user‑facing strings; move to translations/util string helper.


64-66: Tighten typing for featureKey.

Use a keyed type from the form model (e.g., keyof MeetingFeaturesFormControls) instead of string to catch template typos at compile time.

apps/lfx-one/src/server/services/microservice-proxy.service.ts (3)

30-33: Guard path and join safely to avoid double slashes and invalid inputs.

Ensure path is a relative path starting with / and normalize the join.

+  private static joinEndpoint(baseUrl: string, path: string): string {
+    if (!path.startsWith('/')) {
+      throw new Error(`Expected path to start with '/': ${path}`);
+    }
+    return `${baseUrl.replace(/\/+$/, '')}${path}`;
+  }
@@
-      const endpoint = `${baseUrl}${path}`;
+      const endpoint = MicroserviceProxyService.joinEndpoint(baseUrl, path);
@@
-      const endpoint = `${baseUrl}${path}`;
+      const endpoint = MicroserviceProxyService.joinEndpoint(baseUrl, path);

Also applies to: 69-72


25-41: Deduplicate endpoint/operation/query assembly.

The two methods repeat URL/env/query logic. Extract a small helper to reduce drift.

 export class MicroserviceProxyService {
   private apiClient: ApiClientService = new ApiClientService();
+  private readonly MICROSERVICE_URLS: MicroserviceUrls = {
+    LFX_V2_SERVICE: process.env['LFX_V2_SERVICE'] || 'http://lfx-api.k8s.orb.local',
+  };
+
+  private prepare<T>(
+    req: Request,
+    service: keyof MicroserviceUrls,
+    path: string,
+    method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
+    query?: Record<string, any>
+  ) {
+    const operation = `${method.toLowerCase()}_${path.replace(/\//g, '_')}`;
+    const baseUrl = this.MICROSERVICE_URLS[service];
+    const endpoint = MicroserviceProxyService.joinEndpoint(baseUrl, path);
+    const token = (req as any).bearerToken;
+    const mergedQuery = { ...query, ...DEFAULT_QUERY_PARAMS };
+    return { operation, endpoint, token, mergedQuery };
+  }
@@
-    const operation = `${method.toLowerCase()}_${path.replace(/\//g, '_')}`;
-    try {
-      const MICROSERVICE_URLS: MicroserviceUrls = {
-        LFX_V2_SERVICE: process.env['LFX_V2_SERVICE'] || 'http://lfx-api.k8s.orb.local',
-      };
-      const baseUrl = MICROSERVICE_URLS[service];
-      const endpoint = `${baseUrl}${path}`;
-      const token = req.bearerToken;
-      const mergedQuery = { ...query, ...DEFAULT_QUERY_PARAMS };
+    const { operation, endpoint, token, mergedQuery } = this.prepare<T>(req, service, path, method, query);
+    try {
       req.log.debug({ path: endpoint, method, mergedQuery, data, customHeaders }, 'Proxy request');
@@
-    const operation = `${method.toLowerCase()}_${path.replace(/\//g, '_')}`;
-    try {
-      const MICROSERVICE_URLS: MicroserviceUrls = {
-        LFX_V2_SERVICE: process.env['LFX_V2_SERVICE'] || 'http://lfx-api.k8s.orb.local',
-      };
-      const baseUrl = MICROSERVICE_URLS[service];
-      const endpoint = `${baseUrl}${path}`;
-      const token = req.bearerToken;
-      const mergedQuery = { ...query, ...DEFAULT_QUERY_PARAMS };
+    const { operation, endpoint, token, mergedQuery } = this.prepare<T>(req, service, path, method, query);
+    try {

Also applies to: 64-78


44-47: Broaden error guard to common HTTP client shapes.

If ApiClientService sometimes wraps Axios/fetch errors, consider deriving status/errorBody from error.response as a fallback.

-      if (error.status && error.code) {
-        throw MicroserviceError.fromMicroserviceResponse(error.status, error.message, error.errorBody, service, path, operation);
+      const status = error?.status ?? error?.response?.status;
+      const errorBody = error?.errorBody ?? error?.response?.data;
+      if (typeof status === 'number') {
+        throw MicroserviceError.fromMicroserviceResponse(status, error.message, errorBody, service, path, operation);
       }

Also applies to: 81-83

apps/lfx-one/src/app/modules/project/meetings/components/meeting-delete-confirmation/meeting-delete-confirmation.component.ts (2)

33-37: Guard against missing dialog data (possible undefined access).
this.dialogConfig.data?.meeting can be undefined but is typed as Meeting, and subsequent fields dereference it. Add a hard guard and assign in ctor.

Apply:

-  public readonly meeting: Meeting = this.dialogConfig.data?.meeting;
+  public readonly meeting!: Meeting;
...
-  public readonly registrantCount: number = this.meeting.individual_registrants_count + this.meeting.committee_members_count;
+  public readonly registrantCount: number = this.meeting.individual_registrants_count + this.meeting.committee_members_count;

And add a constructor (after injections):

+  public constructor() {
+    if (!this.dialogConfig.data?.meeting) {
+      throw new Error('Meeting data is required to delete.');
+    }
+    this.meeting = this.dialogConfig.data.meeting as Meeting;
+  }

99-103: Strongly type the form control to avoid any from AbstractControl.value.
Prevents accidental invalid delete types.

Apply:

-  public readonly deleteForm: FormGroup = this.initializeDeleteForm();
+  public readonly deleteForm: FormGroup<{ deleteType: FormControl<'single' | 'series' | 'future'> }> = this.initializeDeleteForm();
...
-  private initializeDeleteForm(): FormGroup {
-    return new FormGroup({
-      deleteType: new FormControl('single'),
-    });
-  }
+  private initializeDeleteForm(): FormGroup<{ deleteType: FormControl<'single' | 'series' | 'future'> }> {
+    return new FormGroup<{ deleteType: FormControl<'single' | 'series' | 'future'> }>({
+      deleteType: new FormControl<'single' | 'series' | 'future'>('single', { nonNullable: true }),
+    });
+  }

Also applies to: 42-43

apps/lfx-one/src/server/services/ai.service.ts (1)

82-86: Preserve original error as cause.
Improves observability without leaking details to callers.

Apply:

-      throw new Error('Failed to generate meeting agenda');
+      throw new Error('Failed to generate meeting agenda', { cause: error as Error });
apps/lfx-one/src/app/shared/components/header/header.component.ts (1)

122-125: Drop console noise in production.
Replace with a UI action or remove.

-    console.info('User avatar clicked');
+    // TODO: open user menu / navigate to profile
apps/lfx-one/src/app/shared/pipes/linkify.pipe.ts (2)

6-6: Avoid barrel import from '@lfx-one/shared' if possible.
Prefer direct path import to prevent unintended deps and tree-shaking issues.

If available, switch to the concrete module path (e.g., @lfx-one/shared/utils/extract-urls).


12-38: Return type mismatch: function returns string, signature says SafeHtml.
Either return SafeHtml or change the signature to string.

Apply one of:

Option A (return SafeHtml):

-  public transform(value: string | null | undefined): SafeHtml {
+  public transform(value: string | null | undefined): SafeHtml {
...
-    return this.sanitizer.sanitize(SecurityContext.HTML, linkedText) || '';
+    const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, linkedText) || '';
+    return this.sanitizer.bypassSecurityTrustHtml(sanitized);

Option B (simpler, return string):

-  public transform(value: string | null | undefined): SafeHtml {
+  public transform(value: string | null | undefined): string {
...
-    return this.sanitizer.sanitize(SecurityContext.HTML, linkedText) || '';
+    return this.sanitizer.sanitize(SecurityContext.HTML, linkedText) || '';

Please pick one based on template usage.

apps/lfx-one/src/server/services/supabase.service.ts (1)

22-22: Optional: avoid barrel-style interface imports for consistency.

If @lfx-one/shared/interfaces is a barrel, prefer direct paths (e.g., @lfx-one/shared/interfaces/user) to reduce import cycles and improve tree-shaking. This follows past guidance captured in learnings.

apps/lfx-one/src/app/shared/services/project.service.ts (1)

6-6: Optional: prefer direct type imports over interface barrel.

If available, import Project from its concrete module (e.g., @lfx-one/shared/interfaces/project) to avoid barrel coupling.

apps/lfx-one/src/app/modules/project/meetings/components/meeting-committee-modal/meeting-committee-modal.component.ts (2)

11-11: Align VOTING_STATUSES import with shared constants path.

Elsewhere we import constants from @lfx-one/shared/constants. For consistency, import VOTING_STATUSES from the constants module rather than the package root.

Apply:

-import { VOTING_STATUSES } from '@lfx-one/shared';
+import { VOTING_STATUSES } from '@lfx-one/shared/constants';

16-18: PrimeNG direct imports — consider using LFX wrappers.

Per prior team guidance, prefer LFX wrapper components/services over direct PrimeNG usage where wrappers exist (DynamicDialog, Tooltip, MessageService).

Do wrappers exist for these in this repo? If yes, we should switch in a follow-up to keep consistency.

apps/lfx-one/src/app/shared/components/header/header.component.html (1)

12-12: Rename brittle test-id to a brand-agnostic token.

Avoid future churn by using a neutral id.

Apply:

-<span class="text-xl font-display text-gray-900 hidden md:block" data-testid="header-projects-text">One</span>
+<span class="text-xl font-display text-gray-900 hidden md:block" data-testid="header-brand-text">One</span>

Remember to update E2E selectors accordingly.

docs/architecture/backend/README.md (1)

74-76: Inconsistent logger mention: use Pino, not Winston.
Doc earlier cites Pino; fix this section for consistency.

-Explore Winston logging configuration, structured logging, and monitoring strategies.
+Explore Pino logging configuration, structured logging, and monitoring strategies.
apps/lfx-one/src/app/shared/components/avatar/avatar.component.ts (1)

22-22: Avoid null default for style input.
Prefer undefined to prevent “null” leaking into DOM bindings.

-  public readonly style = input<AvatarProps['style']>(null);
+  public readonly style = input<AvatarProps['style']>(undefined as unknown as AvatarProps['style']);
apps/lfx-one/src/server/services/etag.service.ts (1)

19-31: Consider HEAD for ETag fetch when supported.
Fetching via HEAD can reduce payload when only the ETag is needed.

apps/lfx-one/src/app/shared/pipes/recurrence-summary.pipe.ts (1)

52-55: Harden parsing of weekly_days to avoid NaN entries.

If malformed data sneaks in (e.g., stray commas/spaces), parseInt(...) - 1 can produce NaN. Filter and bounds-check before subtracting.

Apply this diff:

-    let weeklyDaysArray: number[] = [];
-    if (recurrence.weekly_days) {
-      weeklyDaysArray = recurrence.weekly_days.split(',').map((d) => parseInt(d.trim()) - 1);
-    }
+    let weeklyDaysArray: number[] = [];
+    if (recurrence.weekly_days) {
+      weeklyDaysArray = recurrence.weekly_days
+        .split(',')
+        .map((d) => Number(d.trim()))
+        .filter((n) => Number.isInteger(n) && n >= 1 && n <= 7)
+        .map((n) => n - 1);
+    }
apps/lfx-one/src/app/modules/project/meetings/components/meeting-recurrence-pattern/meeting-recurrence-pattern.component.ts (1)

71-73: Use numeric comparator with sort (nit).

Explicit comparator avoids lexicographic surprises and future regressions if values exceed single digits.

Apply this diff:

-      newDays = [...currentDays, dayIndex].sort();
+      newDays = [...currentDays, dayIndex].sort((a, b) => a - b);
apps/lfx-one/src/server/routes/permissions.route.ts (2)

52-56: Incorrect operation label in validation error payload.

This is a create call, not get. Fixing improves traceability.

Apply this diff:

-      const validationError = ServiceValidationError.forField('projectId', 'Project ID is required', {
-        operation: 'get_project_permissions',
+      const validationError = ServiceValidationError.forField('projectId', 'Project ID is required', {
+        operation: 'create_user_permissions',
         service: 'permissions_route',
         path: req.path,
       });

61-75: Consider using ServiceValidationError for 400 responses to keep error format consistent.

Current 400s return ad-hoc JSON. Using ServiceValidationError.fromFieldErrors(...) would standardize client handling.

If you want, I can provide a small refactor patch to replace these blocks with ServiceValidationError for uniform API errors. Confirm preference.

Also applies to: 106-121

.github/workflows/e2e-tests.yml (1)

288-291: Correct the skipped‑secrets message for AI config.

The workflow uses GitHub secrets AI_API_KEY and AI_PROXY_URL, not an AWS “ai_config” secret. Update the message to avoid confusion.

Apply this diff:

-          echo "  - /cloudops/managed-secrets/ai/ai_config (AI configuration)"
+          echo "  - AI_API_KEY (GitHub Secret)"
+          echo "  - AI_PROXY_URL (GitHub Secret)"
charts/lfx-v2-ui/README.md (1)

1-3: Header/intro rebrand — LGTM; add note about PCC_ env vars being legacy but retained.*

Most of the doc still uses PCC_* variables and “PCC Application Configuration.” Add a brief note near the top clarifying these names are legacy and intentionally preserved for compatibility, plus any deprecation plan.

docs/architecture/backend/ai-service.md (1)

5-6: Model/proxy note — clarify compatibility with Helm defaults

Helm README defaults AI_PROXY_URL to 'https://api.openai.com/v1/chat/completions' (charts/lfx-v2-ui/README.md lines 44–46) and the params note says "OpenAI compatible" (lines 155–156). Since docs/architecture/backend/ai-service.md describes Claude Sonnet 4 via LiteLLM (lines 5–6), add a single sentence to that doc and update the Helm README: state explicitly that an OpenAI‑compatible proxy (e.g., LiteLLM) is required for Anthropic models and AI_PROXY_URL must point to that proxy (not api.openai.com); replace the README default or show a proxy example/warning.

apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts (1)

182-185: Harden filename validation (reject slashes/control chars).

Prevents odd paths like “a/b.pdf”, backslashes, and control chars.

-    if (file.name.includes('..') || file.name.startsWith('.')) {
+    if (/[\/\\\x00-\x1F]/.test(file.name) || file.name.includes('..') || file.name.startsWith('.')) {
       return `Invalid filename "${file.name}". Filename cannot contain path traversal characters or start with a dot.`;
     }
docs/architecture/shared/package-architecture.md (1)

65-71: Docs promote barrel imports; align with repo guidance (prefer direct paths).

Switch examples to category paths to avoid barrels.

-// Main export (all interfaces)
-import { User, AuthContext, AvatarProps } from '@lfx-one/shared';
+// Prefer category exports
+import { User, AuthContext, AvatarProps } from '@lfx-one/shared/interfaces';

-// Specific exports
-import { User, AuthContext } from '@lfx-one/shared/interfaces';
-import { lfxColors } from '@lfx-one/shared/constants';
+// Specific exports
+import { User, AuthContext } from '@lfx-one/shared/interfaces';
+import { lfxColors } from '@lfx-one/shared/constants';

-// Angular component
-import { User, AvatarProps } from '@lfx-one/shared/interfaces';
+// Angular component
+import { User, AvatarProps } from '@lfx-one/shared/interfaces';

-// Express server
-import { User, AuthContext } from '@lfx-one/shared/interfaces';
+// Express server
+import { User, AuthContext } from '@lfx-one/shared/interfaces';

Also applies to: 226-228, 243-243

apps/lfx-one/src/app/app.component.ts (1)

40-40: Fix comment typo.

“auth s tate” → “auth state”.

-    // Hydrate the auth s tate from the server, if it exists, otherwise set it to false and null
+    // Hydrate the auth state from the server, if it exists; otherwise set it to false and null
apps/lfx-one/src/app/modules/project/meetings/components/recurring-meeting-edit-options/recurring-meeting-edit-options.component.ts (1)

46-50: Optional: strongly type the reactive form.

Improves safety when reading editType.

-  public readonly editForm: FormGroup = this.initializeEditForm();
+  public readonly editForm: FormGroup<{ editType: FormControl<'single' | 'future'> }> = this.initializeEditForm();

-  private initializeEditForm(): FormGroup {
-    return new FormGroup({
-      editType: new FormControl('single'),
-    });
-  }
+  private initializeEditForm(): FormGroup<{ editType: FormControl<'single' | 'future'> }> {
+    return new FormGroup({
+      editType: new FormControl<'single' | 'future'>('single', { nonNullable: true }),
+    });
+  }
apps/lfx-one/src/app/shared/components/time-picker/time-picker.component.ts (1)

8-11: Use LFX wrapper Button/InputText in this component; leave Popover as PrimeNG.

Replace primeng ButtonModule and InputTextModule in apps/lfx-one/src/app/shared/components/time-picker/time-picker.component.ts (import lines ~8–11 and the imports array at ~26) with the existing ButtonComponent and InputTextComponent to preserve LFX theming/consistency. No shared Popover wrapper was found in the repo — keep PopoverModule/Popover usage.

apps/lfx-one/src/app/shared/services/activity.service.ts (1)

6-6: Import rename is fine; consider using a logging service instead of console.error.

Swapping console.error for the project’s logger improves consistency and observability.

apps/lfx-one/src/app/modules/meeting/meeting-join/meeting-join.component.ts (2)

261-265: Avoid deprecated unescape and ensure Unicode‑safe Base64.

Replace unescape/encodeURIComponent/btoa combo with TextEncoder-based encoding.

Apply this diff:

-    const encodedName = btoa(unescape(encodeURIComponent(displayName)));
+    const utf8 = new TextEncoder().encode(displayName);
+    const encodedName = btoa(String.fromCharCode(...utf8));

17-17: Prefer subpath imports over the package barrel for tree‑shaking and clarity.

Import types from @lfx-one/shared/interfaces and utilities from their subpath (e.g., validators/utils) instead of '@lfx-one/shared'.

Apply this diff:

-import { extractUrlsWithDomains, Meeting, Project, User } from '@lfx-one/shared';
+import { Meeting, Project, User } from '@lfx-one/shared/interfaces';
+import { extractUrlsWithDomains } from '@lfx-one/shared/validators' /* or the correct subpath where it lives */;

Please confirm the correct subpath for extractUrlsWithDomains within the shared package.

docs/architecture/shared/development-workflow.md (2)

56-61: Align Yarn/Node versions with root package.json.

Docs show packageManager [email protected] and engines.node >=18, but root uses [email protected] and node >=22. Update examples to avoid on‑boarding confusion.

Apply:

 // package.json
 {
-  "workspaces": ["apps/*", "packages/*"],
-  "packageManager": "[email protected]"
+  "workspaces": ["apps/*", "packages/*"],
+  "packageManager": "[email protected]"
 }
 // Root package.json (example block)
   "engines": {
-    "node": ">=18"
+    "node": ">=22"
   }

Also applies to: 259-267


367-373: Use the correct Turbo cache-clearing command.

“npx turbo prune” is for workspace pruning, not cache clearing. Use “turbo clean” here.

-# Clear Turborepo cache
-npx turbo prune
+# Clear Turborepo cache
+npx turbo clean
docs/architecture.md (3)

176-179: Resolve guidance conflict: direct PrimeNG vs wrapper components.

Top section says “Direct PrimeNG integration,” while later sections mandate LFX wrapper components (and past learnings prefer wrappers). Make the guidance consistent: recommend wrappers everywhere and reserve direct PrimeNG only inside wrappers.

- - **Direct PrimeNG integration** with custom LFX theming
+ - **Use LFX wrapper components**; avoid importing PrimeNG directly in app code

Also applies to: 556-666


914-925: Environment variable prefix still “PCC_*”. Consider alias or rename plan.

Rebrand is complete elsewhere, but envs remain PCC_ (e.g., PCC_BASE_URL). Either:

  • keep PCC_* for backward compatibility and document it, or
  • add support for LFX_ONE_* with PCC_* as fallback and set a deprecation window.
// server.ts (example)
const BASE_URL = process.env['LFX_ONE_BASE_URL'] ?? process.env['PCC_BASE_URL'] ?? 'http://localhost:4200';

Also applies to: 1063-1066


1051-1079: Port consistency: 4000 vs 4200.

Docs and PM2 examples often use 4200, but code samples default to 4000. Align examples or add a note that PORT controls the runtime port and default is 4000.

docs/troubleshooting.md (1)

174-182: Forward Turbo flags correctly through Yarn.

To pass flags to Turbo via Yarn script, use “--”.

-# Force rebuild without cache
-yarn build --force
+# Force rebuild without cache
+yarn build -- --force
docs/deployment.md (2)

255-267: Repo path after clone likely incorrect.

“cd lfx-one” probably should be the repo directory (e.g., lfx-v2-ui). Update to avoid copy-paste errors.

-git clone <your-repo>
-cd lfx-one
+git clone <your-repo>
+cd lfx-v2-ui

97-112: Port defaults vs examples.

PM2 env sets PORT=4200 while startServer defaults to 4000. Add a one-liner clarifying that PORT controls runtime and examples use 4200.

Also applies to: 121-126

docs/architecture/backend/authentication.md (1)

11-25: PCC_ env variables under new branding—document compatibility or add alias.*

Keep PCC_* for now but document the decision and (optionally) support LFX_ONE_* with PCC_* fallback.

Also applies to: 31-33

apps/lfx-one/e2e/project-dashboard.spec.ts (1)

42-42: Title assertion updated — OK; consider making it case-insensitive (optional)

Keeps tests resilient if minor casing/prefix changes happen.

Apply this diff if you agree:

-        await expect(page).toHaveTitle('LFX One');
+        await expect(page).toHaveTitle(/LFX One/);
apps/lfx-one/src/app/modules/project/meetings/meeting-dashboard/meeting-dashboard.component.ts (1)

15-15: Imports migrated — consider typing visibility with enum to avoid string literals

Using MeetingVisibility improves safety where 'public'/'private' strings are used.

Minimal changes:

+import { MeetingVisibility } from '@lfx-one/shared/enums';

Example updates elsewhere in this file (illustrative):

// options
const VISIBILITY_OPTIONS: { label: string; value: MeetingVisibility | null }[] = [
  { label: 'All Visibilities', value: null },
  { label: 'Public', value: MeetingVisibility.PUBLIC },
  { label: 'Private', value: MeetingVisibility.PRIVATE },
];

// filters
filtered = filtered.filter((m) => m.visibility === MeetingVisibility.PUBLIC);

// colors
backgroundColor: m.visibility === MeetingVisibility.PUBLIC ? '#3b82f6' : '#6b7280',
apps/lfx-one/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts (1)

13-13: Import Committee from its concrete interface file (avoid barrel).

Committee is declared in packages/shared/src/interfaces/committee.interface.ts — update apps/lfx-one/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts (line 13) to:
import { Committee } from '@lfx-one/shared/interfaces/committee.interface'

apps/lfx-one/src/app/modules/project/meetings/components/meeting-registrants/meeting-registrants.component.ts (1)

16-17: Prefer LFX wrapper over direct PrimeNG where available.

If a wrapper exists for ConfirmDialog/ConfirmationService, use it for consistency; otherwise ignore.

Do we have LFX wrappers for ConfirmDialog in this repo?

apps/lfx-one/src/server/server.ts (2)

153-168: Env var names still use PCC_ — confirm intent; optionally support LFX_ONE_ fallbacks.**

If branding requires envs to change, add backward‑compatible fallbacks now to avoid breaking deployments.

Proposed minimal shim:

-const authConfig: ConfigParams = {
+const BASE_URL = process.env['LFX_ONE_BASE_URL'] ?? process.env['PCC_BASE_URL'] ?? 'http://localhost:4000';
+const AUTH0_CLIENT_ID = process.env['LFX_ONE_AUTH0_CLIENT_ID'] ?? process.env['PCC_AUTH0_CLIENT_ID'] ?? '1234';
+const AUTH0_ISSUER_BASE_URL = process.env['LFX_ONE_AUTH0_ISSUER_BASE_URL'] ?? process.env['PCC_AUTH0_ISSUER_BASE_URL'] ?? 'https://example.com';
+const AUTH0_SECRET = process.env['LFX_ONE_AUTH0_SECRET'] ?? process.env['PCC_AUTH0_SECRET'] ?? 'sufficiently-long-string';
+const AUTH0_CLIENT_SECRET = process.env['LFX_ONE_AUTH0_CLIENT_SECRET'] ?? process.env['PCC_AUTH0_CLIENT_SECRET'] ?? 'bar';
+const AUTH0_AUDIENCE = process.env['LFX_ONE_AUTH0_AUDIENCE'] ?? process.env['PCC_AUTH0_AUDIENCE'] ?? 'https://example.com';
+const authConfig: ConfigParams = {
   authRequired: false,
   auth0Logout: true,
-  baseURL: process.env['PCC_BASE_URL'] || 'http://localhost:4000',
-  clientID: process.env['PCC_AUTH0_CLIENT_ID'] || '1234',
-  issuerBaseURL: process.env['PCC_AUTH0_ISSUER_BASE_URL'] || 'https://example.com',
-  secret: process.env['PCC_AUTH0_SECRET'] || 'sufficiently-long-string',
+  baseURL: BASE_URL,
+  clientID: AUTH0_CLIENT_ID,
+  issuerBaseURL: AUTH0_ISSUER_BASE_URL,
+  secret: AUTH0_SECRET,
   authorizationParams: {
     response_type: 'code',
-    audience: process.env['PCC_AUTH0_AUDIENCE'] || 'https://example.com',
+    audience: AUTH0_AUDIENCE,
     scope: 'openid email profile access:api offline_access',
   },
-  clientSecret: process.env['PCC_AUTH0_CLIENT_SECRET'] || 'bar',
+  clientSecret: AUTH0_CLIENT_SECRET,
   routes: { login: false },
 };
@@
-    const validatedReturnTo = validateAndSanitizeUrl(returnTo, [process.env['PCC_BASE_URL'] as string]);
+    const validatedReturnTo = validateAndSanitizeUrl(returnTo, [BASE_URL]);
@@
-        { provide: APP_BASE_HREF, useValue: process.env['PCC_BASE_URL'] },
+        { provide: APP_BASE_HREF, useValue: BASE_URL },

Also update docs to state supported env names.

Also applies to: 247-247, 172-190, 175-176, 183-185


126-147: Nit: autoLogging ignore includes /api/health but only /health is defined here.

Either add /api/health route or remove it from the ignore for clarity.

apps/lfx-one/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.ts (1)

21-21: Prefer LFX wrapper for Tooltip if available.

If a wrapper exists for primeng/tooltip, use it for consistency across the app.

Confirm whether an LFX Tooltip wrapper exists in this workspace.

apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts (3)

11-11: Avoid barrel import; use subpath for interfaces

Prefer direct subpath to match repo conventions and aid treeshaking.

-import { CreateUserPermissionRequest, PermissionLevel, PermissionScope, UserPermissionSummary } from '@lfx-one/shared';
+import { CreateUserPermissionRequest, PermissionLevel, PermissionScope, UserPermissionSummary } from '@lfx-one/shared/interfaces';

153-167: Committee list can fetch with undefined project UID; make it reactive to project signal

Initialize after project is available to avoid calling the service with "undefined" and keep it reactive.

-  private initCommittees(): Signal<{ label: string; value: string }[]> {
-    return toSignal(
-      this.committeeService.getCommitteesByProject(this.project()?.uid as string).pipe(
-        take(1),
-        map((committees) =>
-          committees.map((committee) => ({
-            label: committee.name,
-            value: committee.uid,
-          }))
-        )
-      ),
-      {
-        initialValue: [],
-      }
-    );
-  }
+  private initCommittees(): Signal<{ label: string; value: string }[]> {
+    return toSignal(
+      toObservable(this.project).pipe(
+        map((p) => p?.uid),
+        filter((uid): uid is string => !!uid),
+        distinctUntilChanged(),
+        switchMap((uid) => this.committeeService.getCommitteesByProject(uid)),
+        map((committees) =>
+          committees.map((c) => ({
+            label: c.name,
+            value: c.uid,
+          }))
+        )
+      ),
+      { initialValue: [] }
+    );
+  }

Note: add these imports:

-import { toSignal } from '@angular/core/rxjs-interop';
+import { toObservable, toSignal } from '@angular/core/rxjs-interop';
-import { map, take } from 'rxjs';
+import { distinctUntilChanged, filter, map, switchMap } from 'rxjs';

18-18: PrimeNG direct import — prefer LFX wrapper if available

If an LFX Tooltip wrapper exists, use it for consistency.

Do we have an LFX Tooltip wrapper component/directive to replace primeng/tooltip here?

apps/lfx-one/src/server/routes/meetings.route.ts (4)

4-4: Avoid barrel import; use subpaths for constants/utils

Split to match shared package structure.

-import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE_BYTES, sanitizeFilename } from '@lfx-one/shared';
+import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE_BYTES } from '@lfx-one/shared/constants';
+import { sanitizeFilename } from '@lfx-one/shared/utils';

189-196: Don’t trust client-provided file size

Always persist the server-computed size.

-      file_size: fileSize || buffer.length,
+      file_size: buffer.length,

280-285: Incorrect param key in error log (uses id instead of uid)

Fixes misleading telemetry.

-        meeting_id: req.params['id'],
+        meeting_id: req.params['uid'],

406-410: Incorrect param key in error log (uses id instead of uid)

Same logging fix in attachments fetch error path.

-        meeting_id: req.params['id'],
+        meeting_id: req.params['uid'],
apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts (2)

16-16: Avoid barrel import; use subpaths for utils and interfaces

Keeps imports clear and prevents dragging extra surface.

-import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingOccurrence, MeetingRegistrant } from '@lfx-one/shared';
+import { extractUrlsWithDomains } from '@lfx-one/shared/utils';
+import { Meeting, MeetingAttachment, MeetingOccurrence, MeetingRegistrant } from '@lfx-one/shared/interfaces';

20-25: PrimeNG direct modules — prefer LFX wrappers if available

Stay consistent with wrapper usage across the app.

Are there LFX wrapper components/modules for Tooltip, ConfirmDialog, or AnimateOnScroll we should use here?

apps/lfx-one/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts (3)

31-39: Potential read-before-assign on upcomingMeeting.
committees computed is created in the constructor and reads this.upcomingMeeting() which is only assigned in ngOnInit. Initialize upcomingMeeting to a safe default.

Apply:

- import { Component, computed, inject, Injector, input, OnInit, runInInjectionContext, Signal } from '@angular/core';
+ import { Component, computed, inject, Injector, input, OnInit, runInInjectionContext, Signal, signal } from '@angular/core';

- public upcomingMeeting!: Signal<Meeting | null>;
+ public upcomingMeeting: Signal<Meeting | null> = signal<Meeting | null>(null);

69-82: Guard against empty results when filtering by committeeId.
committeeMeetings[0] can be undefined; return null instead to match the declared Signal<Meeting | null>.

-            return committeeMeetings.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime())[0];
+            const next = committeeMeetings
+              .sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime())[0];
+            return next ?? null;
@@
-          return meetings.filter((meeting) => new Date(meeting.start_time).getTime() > new Date().getTime())[0];
+          return meetings
+            .filter((meeting) => new Date(meeting.start_time).getTime() > new Date().getTime())[0] ?? null;

60-82: Tiny readability/alloc nit: compute now once.
Avoid repeated new Date().getTime() calls.

-            const committeeMeetings = meetings.filter(
-              (meeting) =>
-                new Date(meeting.start_time).getTime() > new Date().getTime() &&
+            const now = Date.now();
+            const committeeMeetings = meetings.filter(
+              (meeting) =>
+                new Date(meeting.start_time).getTime() > now &&
                 meeting.committees &&
                 meeting.committees?.length > 0 &&
                 meeting.committees.some((c) => c.uid === committeeId)
             );
@@
-          return meetings.filter((meeting) => new Date(meeting.start_time).getTime() > new Date().getTime())[0] ?? null;
+          const now = Date.now();
+          return meetings.filter((meeting) => new Date(meeting.start_time).getTime() > now)[0] ?? null;
apps/lfx-one/src/app/shared/services/committee.service.ts (1)

63-65: Return type vs behavior mismatch for getCommitteeMember.
Signature allows null, but the method never emits null. Either change the return type to Observable<CommitteeMember> or map 404 to null.

-  public getCommitteeMember(committeeId: string, memberId: string): Observable<CommitteeMember | null> {
-    return this.http.get<CommitteeMember>(`/api/committees/${committeeId}/members/${memberId}`);
-  }
+  public getCommitteeMember(committeeId: string, memberId: string): Observable<CommitteeMember | null> {
+    return this.http.get<CommitteeMember>(`/api/committees/${committeeId}/members/${memberId}`).pipe(
+      catchError((error) => {
+        if (error?.status === 404) {
+          return of(null);
+        }
+        return throwError(() => error);
+      })
+    );
+  }
apps/lfx-one/src/server/controllers/meeting.controller.ts (1)

682-733: “Fail-fast on 403” only applies to the first item.
The helper stops early only if the first operation fails with 403; later 403s don’t short-circuit. Either update the comment to reflect behavior or implement true fail-fast for any 403.

-  private async processRegistrantOperations<T, R>(
+  private async processRegistrantOperations<T, R>(
     req: Request,
     next: NextFunction,
     startTime: number,
     operationName: string,
     meetingUid: string,
     inputData: T[],
     operation: (input: T) => Promise<R>,
     getIdentifier: (input: T, index?: number) => string
   ): Promise<{ results: PromiseSettledResult<R>[]; shouldReturn: boolean }> {
-    try {
-      // Process the first registrant
-      const firstResult = await operation(inputData[0]);
-
-      // If the first registrant succeeds, process the remaining in parallel
-      let results: PromiseSettledResult<R>[];
-      if (inputData.length > 1) {
-        const remainingResults = await Promise.allSettled(inputData.slice(1).map((input) => operation(input)));
-        results = [{ status: 'fulfilled', value: firstResult }, ...remainingResults];
-      } else {
-        results = [{ status: 'fulfilled', value: firstResult }];
-      }
-
-      // Return the results and shouldReturn flag
-      return { results, shouldReturn: false };
-    } catch (error: any) {
-      // Check if it's a 403 error - if so, fail fast
-      // This will stop the processing if a 403 error is encountered
-      if (error?.status === 403 || error?.statusCode === 403) {
-        Logger.error(req, operationName, startTime, error, {
-          meeting_uid: meetingUid,
-          identifier: getIdentifier(inputData[0], 0),
-          fail_fast: true,
-        });
-
-        // Send the error to the next middleware
-        next(error);
-        return { results: [], shouldReturn: true };
-      }
-
-      // For other errors, continue processing the remaining items
-      let results: PromiseSettledResult<R>[] = [{ status: 'rejected', reason: error }];
-
-      if (inputData.length > 1) {
-        const remainingResults = await Promise.allSettled(inputData.slice(1).map((input) => operation(input)));
-        results = [...results, ...remainingResults];
-      }
-
-      // Return the results and shouldReturn flag
-      return { results, shouldReturn: false };
-    }
+    const results: PromiseSettledResult<R>[] = [];
+    for (let i = 0; i < inputData.length; i++) {
+      try {
+        const value = await operation(inputData[i]);
+        results.push({ status: 'fulfilled', value } as PromiseFulfilledResult<R>);
+      } catch (error: any) {
+        if (error?.status === 403 || error?.statusCode === 403) {
+          Logger.error(req, operationName, startTime, error, {
+            meeting_uid: meetingUid,
+            identifier: getIdentifier(inputData[i], i),
+            fail_fast: true,
+          });
+          next(error);
+          return { results, shouldReturn: true };
+        }
+        results.push({ status: 'rejected', reason: error } as PromiseRejectedResult);
+      }
+    }
+    return { results, shouldReturn: false };
   }

@github-actions
Copy link

github-actions bot commented Sep 16, 2025

✅ E2E Tests Passed

Browser: chromium
Status: passed

All E2E tests passed successfully.

Test Configuration

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docs/deployment.md (3)

40-46: Fix port mismatch across PM2, server default, and Docker.

Docs currently mix 4200 (PM2/env note) and 4000 (server default + Docker). Recommend standardizing on 4000 for SSR everywhere to avoid broken deployments.

Apply this diff to align the docs:

 module.exports = {
   apps: [
     {
       name: 'lfx-one',
       script: 'apps/lfx-one/dist/lfx-one/server/server.mjs',
       env: {
         PM2: 'true',
         NODE_ENV: 'production',
-        PORT: 4200,
+        PORT: 4000,
       },
-# Port: Exposes port 4200
+# Port: Exposes port 4000
-# Build for specific environment
-docker build --build-arg BUILD_ENV=prod -f Dockerfile -t lfx-one .
+# Build for specific environment (example: prod)
+docker build --build-arg BUILD_ENV=prod -f Dockerfile -t lfx-one .

No change needed to:

  • startServer default (4000) — already correct
  • docker run -p 4000:4000 — already correct


Also applies to: 106-111, 123-126, 141-142, 148-155, 151-151

---

`199-206`: **Production vars: ENV is set to “development”.**

This is misleading in a “Required Production Variables” block. Set to production.


```diff
-ENV=development
+ENV=production

169-176: Verify workflow rename and paths.

  • docs/deployment.md references .github/workflows/deploy-dev.yml (docs/deployment.md:169) but .github/workflows contains no deploy-dev.yml — present workflows include docker-build-main.yml, docker-build-pr.yml, docker-build-tag.yml, e2e-tests.yml, etc. Restore/rename the workflow or update the docs to point to the actual file.
  • Confirm the deployment workflow (and any ECR/image pushes) uses the lfx-one image/repo names and not lfx-pcc. Repo-wide search shows lfx-one in use; a potential leftover appears as a User‑Agent string "LFX-PCC-Server/1.0" in apps/lfx-one/src/server/services/api-client.service.ts — remove or document if intentional.
♻️ Duplicate comments (1)
docs/deployment.md (1)

129-129: Unify Dockerfile path examples; avoid absolute “/path/to/Dockerfile”.

Prior review flagged “Dockerfile-changelog”; you replaced it, but examples are inconsistent (/, /path/to, bare). Use a single, repo‑relative example to reduce confusion.

Apply this diff:

-The application uses a multi-stage Docker build process defined in `/Dockerfile`:
+The application uses a multi-stage Docker build process defined in `Dockerfile`:
-# Build the image
-docker build -f /path/to/Dockerfile -t lfx-one .
+# Build the image
+docker build -f Dockerfile -t lfx-one .

(Keep the later example with -f Dockerfile as-is.)

Also applies to: 148-148, 154-154

🧹 Nitpick comments (1)
docs/deployment.md (1)

258-258: Fix repository directory cd step.

After git clone <your-repo>, cd lfx-one is unlikely to be valid for this repo. Use a placeholder or the actual repo dir.

-git clone <your-repo>
-cd lfx-one
+git clone <your-repo>
+cd <repo-dir>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 94e6be2 and a1d3d7b.

📒 Files selected for processing (3)
  • apps/lfx-one/e2e/project-dashboard-robust.spec.ts (2 hunks)
  • apps/lfx-one/e2e/project-dashboard.spec.ts (2 hunks)
  • docs/deployment.md (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/lfx-one/e2e/project-dashboard-robust.spec.ts
  • apps/lfx-one/e2e/project-dashboard.spec.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
PR: linuxfoundation/lfx-v2-ui#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-05T18:09:48.792Z
Learning: Applies to apps/lfx-pcc/src/**/*.{ts,html} : Use LFX wrapper components instead of importing PrimeNG components directly
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: E2E Tests / Playwright E2E Tests
🔇 Additional comments (5)
docs/deployment.md (5)

12-12: LGTM on output path rename.

The dist path correctly reflects the app rename to lfx-one.


75-76: PM2 commands updated consistently.

Process name lfx-one is consistent across reload/logs/stop/delete.

Also applies to: 79-80, 82-86


97-97: Path reference looks correct.

apps/lfx-one/src/server/server.ts matches the new app folder.


204-214: Legacy PCC_ env var names in One-branded docs — inventory & migration plan required*

docs/deployment.md (lines 204–214) still documents PCC_* env vars (PCC_BASE_URL, PCC_AUTH0_*). Either document these as legacy, or rename to ONE_/LFX_ONE_ and migrate code/env usage with a deprecation window.

Run this inventory from the repo root and paste results here to confirm scope:

#!/bin/bash
set -euo pipefail
# search for direct process.env.PCC_* usage
rg -n -uu -S 'process\.env\.(PCC_[A-Z0-9_]+)' -C2 || true
# search for any PCC_ literal (docs, configs, env files)
rg -n -uu -S '\bPCC_[A-Z0-9_]+\b' -C2 || true
# search for ONE_/LFX_ONE_ variants
rg -n -uu -S '\b(ONE_|LFX_ONE_)[A-Z0-9_]*\b' -C2 || true

179-184: ECS names updated — verify IaC & CI references

Location: docs/deployment.md (lines 179–184)

Confirm Cluster 'lfx-one', Service 'lfx-one-service', and log group '/ecs/lfx-one-logs' are updated in task definitions, Terraform/CloudFormation/Helm charts, and GitHub Actions env vars. Sandbox repo search did not run; manual verification required.

Copy link
Contributor

@dealako dealako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge and pray.

@dealako dealako merged commit 3f789eb into main Sep 16, 2025
10 of 11 checks passed
@dealako dealako deleted the feat/LFXV2-489 branch September 16, 2025 03:31
@github-actions
Copy link

🧹 Deployment Removed

The deployment for PR #87 has been removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants