Skip to content

Conversation

@andrest50
Copy link
Contributor

@andrest50 andrest50 commented Nov 10, 2025

Ticket

LFXV2-711

Summary

Migrate meeting attachment management from Supabase to LFX V2 API, implementing complete backend endpoints and updating frontend components to support the new API integration.

Changes

Backend

  • Add meeting attachment endpoints to controller and service
    • Create file attachment with multipart/form-data support
    • Get meeting attachments list
    • Get individual attachment metadata and file download
    • Delete attachment
  • Implement proper FormData handling with Content-Type and Content-Length headers
  • Add file upload support using form-data package for Node.js
  • Support both file and link attachment types

Frontend

  • Update meeting service to use LFX V2 API endpoints instead of Supabase
  • Add uploaded field to PendingAttachment interface for upload state tracking
  • Improve attachment upload UX in edit mode:
    • Show spinner icon during upload
    • Show green checkmark for successfully uploaded files
    • Keep uploaded files visible in pending attachments list
  • Fix attachment saving logic in create mode (step 4 of wizard)
  • Fix navigation flow to properly proceed to step 5 (Manage Guests) after creating meeting with attachments
  • Prevent duplicate uploads by filtering already-uploaded files
  • Update dashboard meeting cards to display file attachments with proper download links
  • Add file type icons and tooltips for attachment display

Generated with Claude Code

Migrate meeting attachments from Supabase to LFX V2 API endpoints with comprehensive functionality improvements.

**Backend Changes:**
- Add meeting attachment controller with create, list, get, and download endpoints
- Implement Query Service integration for attachment listing with proper filtering
- Add FormData support in ApiClientService for multipart file uploads
- Fix content-type handling by using file_content_type field
- Implement RFC 5987 encoding for Content-Disposition headers to handle special characters in filenames
- Add proper error handling and logging for attachment operations
- Restructure download logic to prioritize file retrieval over metadata

**Frontend Changes:**
- Convert attachment loading to reactive pattern using Angular signals
- Update meeting service to use new /api/meetings/:uid/attachments endpoints
- Add PendingAttachment interface for upload state tracking
- Implement proper error handling and user feedback for attachment operations
- Update all attachment display components to use new data structure
- Add proper download links with target="_blank" and download attributes

**Key Fixes:**
- Fix attachment filtering to show correct attachments per meeting using tags parameter
- Fix file download Content-Disposition header to properly encode filenames with spaces and special characters
- Fix content-type preservation for uploaded files (e.g., PNG, PDF)
- Make metadata fetching non-blocking in download endpoint

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

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
- Keep uploaded files visible in edit mode with checkmark indicator
- Prevent duplicate uploads of already-uploaded files
- Fix attachment creation in create mode on step 4
- Ensure proper navigation to guest management step after saving attachments

This fixes issues where files would disappear after upload in edit mode and attachments
wouldn't be created when creating new meetings. Files now show a green checkmark when
successfully uploaded and remain visible in the pending attachments list.

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

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
@andrest50 andrest50 requested a review from jordane as a code owner November 10, 2025 20:24
Copilot AI review requested due to automatic review settings November 10, 2025 20:24
@coderabbitai
Copy link

coderabbitai bot commented Nov 10, 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

This PR migrates meeting attachments from a Supabase storage-based system to a V2 microservice API-based approach. The attachment data model is restructured (id→uid, file_name→name, introducing type and link fields), and frontend components are updated to use the new properties and API download paths. Backend controllers and services are added to handle attachment CRUD operations.

Changes

Cohort / File(s) Summary
Frontend: Attachment Display Templates
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html, apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html, apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html, apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html
Updated attachment loops to iterate by attachment.uid instead of id; replaced file_name with name; changed download href to /api/meetings/{meetingId}/attachments/{uid} for files or use attachment.link for link-type attachments; added conditional rendering for download attributes and icon types (paperclip for links, mime-type for files).
Frontend: Attachment Management Components
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts, apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html
Updated attachment deletion to reference attachment.uid; switched upload flow from createAttachmentFromUrl to createFileAttachment with actual file data; adjusted result processing for new API response structure; added [meetingId] input binding to resources summary components.
Frontend: Meeting Resources Summary Component
apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
Added meetingId input; replaced per-file upload flow to use createFileAttachment(meetingId, file) instead of uploadFileToStorage; updated PendingAttachment tracking to use link field instead of fileUrl on upload success.
Backend: API Infrastructure
apps/lfx-one/src/server/services/api-client.service.ts, apps/lfx-one/src/server/services/microservice-proxy.service.ts
Added binaryRequest() method to ApiClientService for handling binary (Buffer) responses; implemented private binary request flow with FormData support and binary-specific error handling; added proxyBinaryRequest() to MicroserviceProxyService for proxying binary requests to microservices.
Backend: Meeting Attachment Endpoints
apps/lfx-one/src/server/controllers/meeting.controller.ts, apps/lfx-one/src/server/routes/meetings.route.ts
Added six new public controller methods (createMeetingAttachment, getMeetingAttachment, deleteMeetingAttachment, getMeetingAttachmentMetadata, getMeetingAttachments, getPastMeetingAttachments); wired new routes for attachment CRUD at POST/GET/DELETE /:uid/attachments and /:uid/attachments/:attachmentId; removed inline attachment handling and SupabaseService usage.
Backend: Past Meeting Attachments
apps/lfx-one/src/server/controllers/past-meeting.controller.ts, apps/lfx-one/src/server/routes/past-meetings.route.ts
Added getPastMeetingAttachments() controller method; added GET /:uid/attachments route to retrieve past meeting attachments.
Backend: Meeting Service
apps/lfx-one/src/server/services/meeting.service.ts
Added six public methods for microservice-based attachment operations: createMeetingAttachment, getMeetingAttachment, deleteMeetingAttachment, getMeetingAttachmentMetadata, getMeetingAttachments, getPastMeetingAttachments; all methods include structured logging and error handling via microservice proxy.
Backend: Supabase Service Cleanup
apps/lfx-one/src/server/services/supabase.service.ts
Removed storage-related fields (storageUrl, defaultBucket) and all storage methods (uploadFile, getPublicUrl, deleteFile, createMeetingAttachment, getMeetingAttachments, deleteMeetingAttachment); removed imports for UploadFileResponse and attachment-related types.
Shared Interfaces
packages/shared/src/interfaces/meeting-attachment.interface.ts
Restructured MeetingAttachment: renamed iduid, meeting_idmeeting_uid, removed file_name/file_url, added type ('file' | 'link'), name, optional link, and description; updated PendingAttachment to use file: File instead of fileName, added uploaded flag; removed UploadFileResponse; added new PastMeetingAttachment interface.

Sequence Diagram(s)

sequenceDiagram
    participant Frontend
    participant MeetingManageComponent
    participant MeetingService
    participant MicroserviceProxy
    participant ApiClient
    participant V2API as V2 API Server

    Frontend->>MeetingManageComponent: Upload file
    MeetingManageComponent->>MeetingService: createFileAttachment(meetingId, file)
    MeetingService->>MicroserviceProxy: proxyBinaryRequest(V2_MEETINGS, path)
    MicroserviceProxy->>ApiClient: binaryRequest(POST, url, FormData)
    ApiClient->>V2API: POST /meetings/{uid}/attachments (multipart/form-data)
    V2API-->>ApiClient: MeetingAttachment {uid, name, type, link?}
    ApiClient-->>MicroserviceProxy: Buffer response
    MicroserviceProxy-->>MeetingService: Parsed MeetingAttachment
    MeetingService-->>MeetingManageComponent: Observable<MeetingAttachment>
    MeetingManageComponent->>MeetingManageComponent: Update PendingAttachment (link, uploaded=true)

    rect rgb(100, 150, 200)
    note right of Frontend: Download attachment
    Frontend->>Frontend: href=/api/meetings/{uid}/attachments/{attachmentUid}
    Frontend->>MeetingController: GET /:uid/attachments/:attachmentId
    MeetingController->>MeetingService: getMeetingAttachment(meetingUid, attachmentUid)
    MeetingService->>MicroserviceProxy: proxyBinaryRequest(GET)
    MicroserviceProxy->>V2API: GET /meetings/{uid}/attachments/{attachmentUid}
    V2API-->>MicroserviceProxy: Buffer (file data)
    MicroserviceProxy-->>MeetingService: Buffer
    MeetingService-->>MeetingController: Buffer
    MeetingController->>MeetingController: Set Content-Type & Content-Disposition headers
    MeetingController-->>Frontend: Stream file
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Interface breaking changes: MeetingAttachment, PendingAttachment, and UploadFileResponse restructuring affects multiple consuming components; PastMeetingAttachment introduction requires validation.
  • Multi-layer API integration: Binary request handling in ApiClientService and MicroserviceProxyService introduces new data flow paths that require careful review for error handling and edge cases.
  • Dual upload flows: Meeting attachments transition from uploadFileToStorage (Supabase) to createFileAttachment (V2 API) with different result structures; requires tracing through upload/error handling logic in both meeting-manage and meeting-resources-summary components.
  • Controller & route consolidation: Six new attachment endpoints introduced with validation, logging, and error propagation patterns; requires consistency review across createMeetingAttachment, getMeetingAttachment, deleteMeetingAttachment, and metadata operations.
  • File-spread changes: Frontend templates (4 components) and logic (2 components) require careful cross-checking for consistency in uid/name/link usage and API path construction.

Areas requiring extra attention:

  • Binary request implementation and error mapping (api-client.service.ts, microservice-proxy.service.ts)
  • FormData handling with file uploads and multipart boundary construction
  • Content-Type and Content-Disposition header construction for downloads
  • Consistency of UID validation and error handling across all six new attachment endpoints
  • PendingAttachment state transitions during upload (especially error cases and file vs. link detection)
  • Supabase service removal completeness (verify no orphaned references in other services)

Possibly related PRs

Suggested labels

deploy-preview, breaking-change

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(meetings): migrate attachment management to lfx v2 api' clearly and concisely summarizes the main change: migrating attachment management from Supabase to the LFX V2 API.
Description check ✅ Passed The description is well-related to the changeset, providing ticket reference, summary, detailed backend/frontend changes, and test plan that all align with the code modifications.
Linked Issues check ✅ Passed The pull request fully addresses LFXV2-711 objectives: replaces Supabase with LFX backend APIs for attachments, implements create/list/retrieve/delete operations, adds multipart/form-data handling, and maintains file and link attachment support.
Out of Scope Changes check ✅ Passed All changes are within scope of the linked issue LFXV2-711. Backend endpoint implementations, frontend integration updates, interface migrations, and attachment handling improvements directly support the Supabase-to-LFX-V2-API migration objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch andrest50/meeting-attachments

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 migrates meeting attachment management from Supabase to the LFX V2 API, implementing a comprehensive solution for both file uploads and link attachments with improved upload status tracking in the UI.

Key Changes

  • Backend: New meeting attachment endpoints in controller and service for creating, retrieving, and deleting attachments with FormData handling for file uploads
  • Frontend: Updated meeting service to use LFX V2 API, enhanced upload UX with spinner/checkmark indicators, and fixed attachment saving logic in wizard create mode
  • Interface Updates: Modified MeetingAttachment interface to use new field names (uid, meeting_uid, name, type) and added uploaded field to PendingAttachment for state tracking

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/shared/src/interfaces/meeting-attachment.interface.ts Updated interface with new field names and added uploaded field to PendingAttachment; removed UploadFileResponse interface
apps/lfx-one/src/server/services/supabase.service.ts Removed Supabase storage methods and attachment management code as part of migration
apps/lfx-one/src/server/services/meeting.service.ts Added new methods for attachment CRUD operations via LFX V2 API
apps/lfx-one/src/server/services/api-client.service.ts Enhanced to handle FormData with proper Content-Type and Content-Length headers
apps/lfx-one/src/server/routes/meetings.route.ts Simplified routes by delegating to controller methods; removed old Supabase-based endpoints
apps/lfx-one/src/server/controllers/meeting.controller.ts Added comprehensive attachment endpoint handlers with validation
apps/lfx-one/src/app/shared/services/meeting.service.ts Updated to use new LFX V2 API endpoints; added createFileAttachment and updated createAttachmentFromUrl
apps/lfx-one/src/app/shared/components/meeting-card/*.html Updated to use new field names (uid, name, type) and support both file and link attachments
apps/lfx-one/src/app/shared/components/dashboard-meeting-card/*.html Updated to use new field names and proper download links
apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/* Enhanced upload logic to distinguish between edit and create modes; added direct upload in edit mode
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/* Fixed attachment saving flow in create mode and navigation to step 5; improved attachment refresh logic
apps/lfx-one/src/app/modules/meetings/meeting-join/*.html Updated to use new attachment field names and support link attachments

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +967 to +985
// Create FormData for multipart/form-data request
const formDataClass = (await import('form-data')).default;
const formData = new formDataClass();
formData.append('type', attachmentData.type);
formData.append('name', attachmentData.name);

// If file data is provided, include it (base64 encoded file from client)
if (attachmentData.file) {
const buffer = Buffer.from(attachmentData.file, 'base64');
formData.append('file', buffer, {
filename: attachmentData.name,
contentType: attachmentData.file_content_type || 'application/octet-stream',
});
}

// If link is provided instead of file, include it
if (attachmentData.link) {
formData.append('link', attachmentData.link);
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

There's no validation to ensure that when type is 'file', the file field is provided, or when type is 'link', the link field is provided. This could lead to incomplete attachment records. Consider adding validation:

// After type validation
if (attachmentData.type === 'file' && !attachmentData.file) {
  const validationError = ServiceValidationError.forField('file', 'File data is required for file-type attachments', {
    operation: 'create_meeting_attachment',
    service: 'meeting_controller',
    path: req.path,
  });
  Logger.error(req, 'create_meeting_attachment', startTime, validationError);
  return next(validationError);
}

if (attachmentData.type === 'link' && !attachmentData.link) {
  const validationError = ServiceValidationError.forField('link', 'Link URL is required for link-type attachments', {
    operation: 'create_meeting_attachment',
    service: 'meeting_controller',
    path: req.path,
  });
  Logger.error(req, 'create_meeting_attachment', startTime, validationError);
  return next(validationError);
}

Copilot uses AI. Check for mistakes.
Comment on lines 160 to 212
private uploadFilesDirectly(files: File[], meetingId: string): void {
Array.from(files).forEach((file) => {
const validationError = this.validateFile(file);
if (validationError) {
this.messageService.add({
severity: 'error',
summary: 'File Upload Error',
detail: validationError,
life: 5000,
});
return;
}

const pendingAttachment: PendingAttachment = {
id: crypto.randomUUID(),
fileName: file.name,
file: file,
fileSize: file.size,
mimeType: file.type,
uploading: true,
};

this.pendingAttachments.update((current) => [...current, pendingAttachment]);

// Upload directly to LFX V2 API
this.meetingService.createFileAttachment(meetingId, file).subscribe({
next: (attachment) => {
// Mark as successfully uploaded instead of removing
this.pendingAttachments.update((current) => current.map((pa) => (pa.id === pendingAttachment.id ? { ...pa, uploading: false, uploaded: true } : pa)));
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: `File "${file.name}" uploaded successfully`,
life: 3000,
});
// Emit event to parent to refresh attachments list
this.uploadSuccess.emit(attachment);
},
error: (error) => {
this.pendingAttachments.update((current) =>
current.map((pa) => (pa.id === pendingAttachment.id ? { ...pa, uploading: false, uploadError: error.message || 'Upload failed' } : pa))
);
console.error(`Failed to upload ${file.name}:`, error);
this.messageService.add({
severity: 'error',
summary: 'Upload Failed',
detail: `Failed to upload "${file.name}". Please try again.`,
life: 5000,
});
},
});
});
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

The duplicate file detection only checks against pendingAttachments() but not against existingAttachments(). In edit mode, this allows users to upload files that have the same name as already-uploaded files, potentially causing confusion. Consider also checking against existing attachments when in edit mode to prevent duplicate file names.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +97
} catch {
// Fallback: try to use the FormData directly as a stream
const contentLength = data.getLengthSync?.();
if (contentLength) {
headers['Content-Length'] = String(contentLength);
}
requestInit.body = data as any;
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

The error handling in the FormData buffer conversion swallows all errors silently. If getBuffer() fails, the catch block falls back to using the FormData directly, but there's no logging or indication that an error occurred. Consider adding error logging to help with debugging:

} catch (error) {
  // Log the error for debugging
  console.warn('Failed to get FormData buffer, falling back to stream:', error);
  // Fallback: try to use the FormData directly as a stream
  ...
}

Copilot uses AI. Check for mistakes.
Comment on lines +973 to +980
// If file data is provided, include it (base64 encoded file from client)
if (attachmentData.file) {
const buffer = Buffer.from(attachmentData.file, 'base64');
formData.append('file', buffer, {
filename: attachmentData.name,
contentType: attachmentData.file_content_type || 'application/octet-stream',
});
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

File size and MIME type validation is only performed on the frontend and can be easily bypassed. The backend should validate the decoded file size before creating the buffer and uploading to prevent:

  1. Denial of service attacks with excessively large files
  2. Upload of potentially dangerous file types

Consider adding validation:

// After decoding the base64 file
if (attachmentData.file) {
  const buffer = Buffer.from(attachmentData.file, 'base64');
  
  // Validate file size (e.g., 10MB limit)
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
  if (buffer.length > MAX_FILE_SIZE) {
    const validationError = ServiceValidationError.forField('file', `File size exceeds maximum of ${MAX_FILE_SIZE / 1024 / 1024}MB`, {
      operation: 'create_meeting_attachment',
      service: 'meeting_controller',
      path: req.path,
    });
    Logger.error(req, 'create_meeting_attachment', startTime, validationError);
    return next(validationError);
  }
  
  // Validate content type if provided
  if (attachmentData.file_content_type) {
    const ALLOWED_TYPES = [/* list of allowed MIME types */];
    if (!ALLOWED_TYPES.includes(attachmentData.file_content_type)) {
      const validationError = ServiceValidationError.forField('file_content_type', 'File type not allowed', {
        operation: 'create_meeting_attachment',
        service: 'meeting_controller',
        path: req.path,
      });
      Logger.error(req, 'create_meeting_attachment', startTime, validationError);
      return next(validationError);
    }
  }
  
  formData.append('file', buffer, { ... });
}

Copilot uses AI. Check for mistakes.
[deletingAttachmentId]="deletingAttachmentId()"
[meetingId]="meetingId()"
(goToStep)="goToStep($event)"
(deleteAttachment)="deleteAttachment($event)">
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

The uploadSuccess event is bound in the stepper mode (line 60) but not in the single-page mode (line 119). This means that when a file is uploaded in edit mode using the single-page layout, the onAttachmentUploadSuccess handler won't be called, and the newly uploaded attachment won't be added to the attachments list immediately. Consider adding the event binding for consistency:

<lfx-meeting-resources-summary
  [form]="form()"
  [existingAttachments]="attachments()"
  [isEditMode]="isEditMode()"
  [deletingAttachmentId]="deletingAttachmentId()"
  [meetingId]="meetingId()"
  (goToStep)="goToStep($event)"
  (deleteAttachment)="deleteAttachment($event)"
  (uploadSuccess)="onAttachmentUploadSuccess($event)">
</lfx-meeting-resources-summary>
Suggested change
(deleteAttachment)="deleteAttachment($event)">
(deleteAttachment)="deleteAttachment($event)"
(uploadSuccess)="onAttachmentUploadSuccess($event)">

Copilot uses AI. Check for mistakes.
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 (1)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts (1)

822-846: Add state update to mark successfully uploaded attachments.

The uploaded flag is never set for successful uploads in the meeting-manage component. After savePendingAttachments completes successfully, you must update pendingAttachments with uploaded: true for each successful file. Without this, the filter on line 824 will not exclude already-uploaded files on subsequent saves, creating a duplicate upload vulnerability.

Update handleAttachmentResults to mark successful attachments as uploaded before navigating or moving to the next step:

// After logging failures, add:
successes.forEach((success) => {
  this.pendingAttachments.update((current) =>
    current.map((pa) => (pa.file?.name === success.fileName ? { ...pa, uploaded: true } : pa))
  );
});
🧹 Nitpick comments (3)
apps/lfx-one/src/app/shared/services/meeting.service.ts (3)

249-268: Replace any type with a proper interface.

The attachmentData object on line 253 uses the any type, reducing type safety. Define an interface for the file attachment payload to ensure compile-time validation.

Consider defining an interface like:

interface CreateFileAttachmentPayload {
  type: 'file';
  name: string;
  file: string;  // base64 encoded
  file_content_type: string;
}

Then use it:

-        const attachmentData = {
+        const attachmentData: CreateFileAttachmentPayload = {
           type: 'file',

270-287: Replace any type and remove outdated comment.

Line 274 uses any type for attachmentData, and the comment on lines 271-273 references file-type fields that are not used in this method.

Apply this diff:

   public createAttachmentFromUrl(meetingId: string, name: string, url: string): Observable<MeetingAttachment> {
-    // Build attachment data based on the API schema
-    // For link-type attachments: type, name, link (and optionally description)
-    // For file-type attachments: type, name, file, file_name, file_content_type
-    const attachmentData: any = {
+    // Create link-type attachment
+    const attachmentData = {
       type: 'link' as const,
       name: name,
       link: url,

249-268: Consider adding file size validation.

Before encoding files to base64 (which increases size by ~33%), validate the file size to prevent uploading excessively large files that may exceed API limits or cause performance issues.

Example check before encoding:

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
if (file.size > MAX_FILE_SIZE) {
  return throwError(() => new Error(`File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit`));
}
📜 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 5147707 and b84f685.

📒 Files selected for processing (15)
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html (2 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html (2 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts (8 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html (2 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts (4 hunks)
  • apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html (1 hunks)
  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html (1 hunks)
  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts (1 hunks)
  • apps/lfx-one/src/app/shared/services/meeting.service.ts (1 hunks)
  • apps/lfx-one/src/server/controllers/meeting.controller.ts (1 hunks)
  • apps/lfx-one/src/server/routes/meetings.route.ts (1 hunks)
  • apps/lfx-one/src/server/services/api-client.service.ts (2 hunks)
  • apps/lfx-one/src/server/services/meeting.service.ts (1 hunks)
  • apps/lfx-one/src/server/services/supabase.service.ts (0 hunks)
  • packages/shared/src/interfaces/meeting-attachment.interface.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • apps/lfx-one/src/server/services/supabase.service.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript interfaces instead of union types for better maintainability
When defining PrimeNG-related types, reference the official PrimeNG component interfaces

Files:

  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts
  • apps/lfx-one/src/server/routes/meetings.route.ts
  • apps/lfx-one/src/server/services/api-client.service.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts
  • apps/lfx-one/src/server/controllers/meeting.controller.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
  • packages/shared/src/interfaces/meeting-attachment.interface.ts
  • apps/lfx-one/src/app/shared/services/meeting.service.ts
  • apps/lfx-one/src/server/services/meeting.service.ts
**/*.{ts,tsx,js,jsx,mjs,cjs,html,css,scss}

📄 CodeRabbit inference engine (CLAUDE.md)

Include required license headers on all source files

Files:

  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts
  • apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html
  • apps/lfx-one/src/server/routes/meetings.route.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html
  • apps/lfx-one/src/server/services/api-client.service.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html
  • apps/lfx-one/src/server/controllers/meeting.controller.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html
  • packages/shared/src/interfaces/meeting-attachment.interface.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html
  • apps/lfx-one/src/app/shared/services/meeting.service.ts
  • apps/lfx-one/src/server/services/meeting.service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not nest ternary expressions

Files:

  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts
  • apps/lfx-one/src/server/routes/meetings.route.ts
  • apps/lfx-one/src/server/services/api-client.service.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts
  • apps/lfx-one/src/server/controllers/meeting.controller.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
  • packages/shared/src/interfaces/meeting-attachment.interface.ts
  • apps/lfx-one/src/app/shared/services/meeting.service.ts
  • apps/lfx-one/src/server/services/meeting.service.ts
apps/lfx-one/src/**/*.html

📄 CodeRabbit inference engine (CLAUDE.md)

apps/lfx-one/src/**/*.html: Always add data-testid attributes when creating new Angular components for reliable test targeting
Use data-testid naming convention [section]-[component]-[element]

Files:

  • apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html
  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html
packages/shared/src/interfaces/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Place all TypeScript interfaces in the shared package at packages/shared/src/interfaces

Files:

  • packages/shared/src/interfaces/meeting-attachment.interface.ts
🧠 Learnings (1)
📚 Learning: 2025-10-21T21:19:13.599Z
Learnt from: andrest50
Repo: linuxfoundation/lfx-v2-ui PR: 125
File: apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts:345-350
Timestamp: 2025-10-21T21:19:13.599Z
Learning: In the Angular meeting card component (apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts), when selecting between `summary.summary_data.edited_content` and `summary.summary_data.content`, the logical OR operator (`||`) is intentionally used instead of nullish coalescing (`??`) because empty string edited_content should fall back to the original content rather than being displayed as empty.

Applied to files:

  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts
  • apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html
🧬 Code graph analysis (5)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts (2)
packages/shared/src/interfaces/meeting-attachment.interface.ts (1)
  • MeetingAttachment (8-31)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts (1)
  • importantLinksFormArray (38-40)
apps/lfx-one/src/server/controllers/meeting.controller.ts (1)
apps/lfx-one/src/server/errors/index.ts (1)
  • ServiceValidationError (7-7)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts (1)
packages/shared/src/interfaces/meeting-attachment.interface.ts (2)
  • MeetingAttachment (8-31)
  • PendingAttachment (56-73)
apps/lfx-one/src/app/shared/services/meeting.service.ts (1)
packages/shared/src/interfaces/meeting-attachment.interface.ts (1)
  • MeetingAttachment (8-31)
apps/lfx-one/src/server/services/meeting.service.ts (1)
packages/shared/src/interfaces/api.interface.ts (1)
  • QueryServiceResponse (58-61)
⏰ 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: Agent
🔇 Additional comments (8)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html (1)

57-61: Good call wiring upload success to refresh state

Passing meetingId plus the new uploadSuccess output keeps create-mode UX aligned with the updated resources summary component—nice touch.

apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts (1)

678-691: Reactive attachment stream looks solid

The move to derive attachments from the meeting signal (with a graceful empty fallback) keeps the card in sync without redundant fetches. Nice upgrade.

apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html (1)

83-99: Attachment rendering handles files vs. links nicely

Differentiating download links from external resources (unique track keys, conditional download attr, correct icon) keeps the dashboard card consistent with the new attachment model. Looks good.

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

152-168: LGTM: Signal-based attachments with proper change detection.

The subscription properly updates the attachments signal with a new array reference (line 167), ensuring Angular detects changes within ng-template contexts like p-step-panel. The use of takeUntilDestroyed prevents memory leaks.


250-253: LGTM: Clean integration point for attachment uploads.

This public method provides a clean integration point for child components to notify the parent of successful uploads, updating the signal reactively.


506-513: LGTM: Navigation logic correctly handles create mode on step 4.

In create mode on step 4 (Resources), the flow advances to step 5 (Manage Guests) after saving attachments, rather than immediately returning to the meetings list. This aligns with the PR objectives.


536-537: LGTM: Efficient direct signal mutation for deletion.

Directly filtering the attachments signal is more efficient than triggering a refresh observable and avoids a redundant API call.


371-446: All edge cases properly handled in the refactored flow.

Verification confirms the implementation correctly addresses each concern:

  • Partially uploaded files: The savePendingAttachments() method filters attachments using !attachment.uploading && !attachment.uploadError && !attachment.uploaded && attachment.file, explicitly excluding already-uploaded files.

  • Empty FormArray: The saveImportantLinks() method checks if (!importantLinksFormArray || importantLinksFormArray.length === 0) and returns early.

  • Race conditions: The observable chain completes with take(1) and navigation occurs only after handleAttachmentResults() processes the combined outcome, preventing concurrent operations.

Additionally, the handleAttachmentResults() method comprehensively handles three scenarios: all successes, partial successes with failures, and complete failures, providing proper feedback for each case.

Comment on lines +949 to +985
if (!attachmentData.type || !attachmentData.name) {
const validationError = ServiceValidationError.fromFieldErrors(
{
type: !attachmentData.type ? 'Type is required' : [],
name: !attachmentData.name ? 'Name is required' : [],
},
'Attachment data validation failed',
{
operation: 'create_meeting_attachment',
service: 'meeting_controller',
path: req.path,
}
);

Logger.error(req, 'create_meeting_attachment', startTime, validationError);
return next(validationError);
}

// Create FormData for multipart/form-data request
const formDataClass = (await import('form-data')).default;
const formData = new formDataClass();
formData.append('type', attachmentData.type);
formData.append('name', attachmentData.name);

// If file data is provided, include it (base64 encoded file from client)
if (attachmentData.file) {
const buffer = Buffer.from(attachmentData.file, 'base64');
formData.append('file', buffer, {
filename: attachmentData.name,
contentType: attachmentData.file_content_type || 'application/octet-stream',
});
}

// If link is provided instead of file, include it
if (attachmentData.link) {
formData.append('link', attachmentData.link);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ensure file vs. link payload validation before proxying

We only require type and name, so a type: 'file' payload without file (or a type: 'link' without link) gets proxied to LFX and comes back as a 400/422 from the downstream API. Catching this at the BFF gives us clearer error messaging and avoids an unnecessary round‑trip. Please add explicit guards so that file uploads require attachmentData.file and link uploads require attachmentData.link, returning a local validation error when those conditions aren’t met.

🤖 Prompt for AI Agents
In apps/lfx-one/src/server/controllers/meeting.controller.ts around lines 949 to
985, the controller currently only validates type and name but does not enforce
that type === 'file' requires attachmentData.file and type === 'link' requires
attachmentData.link; add explicit guards after the existing type/name check that
if attachmentData.type === 'file' and no attachmentData.file, or
attachmentData.type === 'link' and no attachmentData.link, you construct a
ServiceValidationError.fromFieldErrors with an appropriate field error (e.g.
file: 'File is required' or link: 'Link is required'), call Logger.error(req,
'create_meeting_attachment', startTime, validationError) and return
next(validationError) so the BFF returns a clear local validation error instead
of proxying to downstream.

- Add PastMeetingAttachment interface to shared package
- Add getPastMeetingAttachments method to backend meeting service
- Add getPastMeetingAttachments controller method to past meeting controller
- Add GET /past-meetings/:uid/attachments route
- Add getPastMeetingAttachments method to frontend meeting service

This enables fetching attachments for past meetings via the LFX V2 API
using the Query Service with type 'past_meeting_attachment'.

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

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
…ing refactor

Removed meeting attachment display and management logic from multiple
components in preparation for a comprehensive refactor. This includes:

- Removed attachment sections from meeting join component
- Removed attachment display from dashboard meeting card
- Removed attachment handling from meeting card component
- Removed attachment upload logic from meeting manage component
- Removed attachment display from meeting resources summary

These changes prepare the codebase for a cleaner implementation of
attachment functionality that will be introduced in subsequent commits.

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

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
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: 2

Caution

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

⚠️ Outside diff range comments (2)
apps/lfx-one/src/app/shared/services/meeting.service.ts (1)

217-248: Remove or deprecate the unused uploadAttachment method.

The uploadAttachment method (lines 217-248) is dead code with zero callers throughout the codebase. It targets the legacy endpoint /attachments/upload while createFileAttachment (lines 250-269) targets the current /attachments endpoint. Since all file uploads now use createFileAttachment, uploadAttachment should either be removed entirely or marked with @deprecated if maintaining backward compatibility is required.

packages/shared/src/interfaces/meeting-attachment.interface.ts (1)

37-50: Remove unused CreateMeetingAttachmentRequest interface.

This interface is not used anywhere in the codebase. Ripgrep search confirms zero references outside its definition. It appears to be superseded by the MeetingAttachment interface and should be removed to maintain code clarity.

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

166-192: Duplicate file detection should check existing attachments.

The duplicate file check (lines 179-184) only validates against pendingAttachments() but not against existingAttachments(). In edit mode, this allows users to upload files with the same name as already-uploaded files.

Apply this diff to check both pending and existing attachments:

-    // Check for duplicate filenames in current session
+    // Check for duplicate filenames in current session and existing attachments
     const currentFiles = this.pendingAttachments();
     const isDuplicate = currentFiles.some((attachment) => attachment.fileName === file.name && !attachment.uploadError);
 
+    // In edit mode, also check against existing attachments
+    if (this.isEditMode()) {
+      const existingFiles = this.existingAttachments();
+      const isDuplicateExisting = existingFiles.some((attachment) => attachment.name === file.name);
+      if (isDuplicateExisting) {
+        return `A file named "${file.name}" already exists for this meeting.`;
+      }
+    }
+
     if (isDuplicate) {
       return `A file named "${file.name}" has already been selected for upload.`;
     }
🧹 Nitpick comments (3)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts (1)

469-470: Consider removing or clarifying the comment.

The comment states "Directly remove the deleted attachment from the signal array" but the actual code triggers a refresh via attachmentsRefresh$.next() rather than directly manipulating the array. The comment may be misleading.

Apply this diff to clarify the comment:

-            // Directly remove the deleted attachment from the signal array
+            // Trigger attachments list refresh after successful deletion
             this.attachmentsRefresh$.next();
apps/lfx-one/src/app/shared/services/meeting.service.ts (2)

271-288: Improve type safety for attachment data.

The attachmentData variable is typed as any (line 275), which reduces type safety. Consider defining a specific interface or inline type for the link attachment payload.

Apply this diff to improve type safety:

  public createAttachmentFromUrl(meetingId: string, name: string, url: string): Observable<MeetingAttachment> {
-   // Build attachment data based on the API schema
-   // For link-type attachments: type, name, link (and optionally description)
-   // For file-type attachments: type, name, file, file_name, file_content_type
-   const attachmentData: any = {
+   const attachmentData: { type: 'link'; name: string; link: string } = {
      type: 'link',
      name: name,
      link: url,
    };

337-339: Consider adding error handling for consistency.

Similar methods like getMeetingAttachments (line 208) include catchError to handle failures gracefully. Consider adding the same pattern here to return an empty array on error.

Apply this diff to add error handling:

  public getPastMeetingAttachments(pastMeetingUid: string): Observable<PastMeetingAttachment[]> {
-   return this.http.get<PastMeetingAttachment[]>(`/api/past-meetings/${pastMeetingUid}/attachments`);
+   return this.http.get<PastMeetingAttachment[]>(`/api/past-meetings/${pastMeetingUid}/attachments`).pipe(
+     catchError((error) => {
+       console.error(`Failed to load attachments for past meeting ${pastMeetingUid}:`, error);
+       return of([]);
+     })
+   );
  }
📜 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 b84f685 and 5473052.

📒 Files selected for processing (16)
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html (2 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.html (1 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts (4 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html (2 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts (2 hunks)
  • apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html (1 hunks)
  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html (1 hunks)
  • apps/lfx-one/src/app/shared/services/meeting.service.ts (3 hunks)
  • apps/lfx-one/src/server/controllers/meeting.controller.ts (1 hunks)
  • apps/lfx-one/src/server/controllers/past-meeting.controller.ts (1 hunks)
  • apps/lfx-one/src/server/routes/meetings.route.ts (1 hunks)
  • apps/lfx-one/src/server/routes/past-meetings.route.ts (1 hunks)
  • apps/lfx-one/src/server/services/api-client.service.ts (3 hunks)
  • apps/lfx-one/src/server/services/meeting.service.ts (1 hunks)
  • apps/lfx-one/src/server/services/microservice-proxy.service.ts (1 hunks)
  • packages/shared/src/interfaces/meeting-attachment.interface.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html
  • apps/lfx-one/src/server/services/api-client.service.ts
  • apps/lfx-one/src/server/services/meeting.service.ts
  • apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html
  • apps/lfx-one/src/server/controllers/meeting.controller.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-21T21:19:13.599Z
Learnt from: andrest50
Repo: linuxfoundation/lfx-v2-ui PR: 125
File: apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts:345-350
Timestamp: 2025-10-21T21:19:13.599Z
Learning: In the Angular meeting card component (apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts), when selecting between `summary.summary_data.edited_content` and `summary.summary_data.content`, the logical OR operator (`||`) is intentionally used instead of nullish coalescing (`??`) because empty string edited_content should fall back to the original content rather than being displayed as empty.

Applied to files:

  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.html
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html
🧬 Code graph analysis (3)
apps/lfx-one/src/server/services/microservice-proxy.service.ts (2)
packages/shared/src/interfaces/api.interface.ts (1)
  • MicroserviceUrls (36-39)
packages/shared/src/constants/api.constants.ts (1)
  • DEFAULT_QUERY_PARAMS (12-15)
apps/lfx-one/src/app/shared/services/meeting.service.ts (1)
packages/shared/src/interfaces/meeting-attachment.interface.ts (2)
  • MeetingAttachment (8-31)
  • PastMeetingAttachment (79-104)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts (1)
packages/shared/src/interfaces/meeting-attachment.interface.ts (1)
  • MeetingAttachment (8-31)
🔇 Additional comments (14)
apps/lfx-one/src/server/routes/past-meetings.route.ts (1)

29-30: LGTM!

The new route for past meeting attachments follows the established pattern and correctly delegates to the controller method.

apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html (1)

238-250: LGTM!

The attachment rendering correctly migrates to the new data model (uid, name) and constructs download URLs using the new API endpoint pattern.

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

68-112: LGTM!

The template correctly migrates to the new attachment data model (uid, name, link) and properly constructs API URLs for download and delete operations.

apps/lfx-one/src/server/controllers/past-meeting.controller.ts (1)

257-298: LGTM!

The new getPastMeetingAttachments controller method follows the established pattern with proper validation, logging, and error handling.

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

90-136: LGTM!

The new proxyBinaryRequest method correctly handles binary responses and follows the established pattern for proxy methods, including proper error transformation and query parameter merging.

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

226-230: LGTM!

The attachment lookup correctly uses the new uid and name properties from the migrated data model.


755-780: LGTM!

The attachment save logic correctly:

  • Filters for unsaved attachments with the file property
  • Uses the new createFileAttachment API
  • Handles successes and failures appropriately
apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.html (1)

269-279: LGTM!

The attachment rendering correctly:

  • Uses attachment.uid for tracking
  • Distinguishes between link and file types
  • Constructs appropriate download URLs for file attachments
  • Displays attachment metadata with proper tooltips
packages/shared/src/interfaces/meeting-attachment.interface.ts (3)

8-31: LGTM! Clean migration to V2 schema.

The interface correctly adopts UID-based identifiers and introduces type discrimination for file vs. link attachments, which improves type safety and clarity.


56-73: LGTM! Upload state tracking properly implemented.

The addition of the file: File field and uploaded?: boolean flag enables proper file handling and upload state tracking, which aligns with the improved upload UX mentioned in the PR objectives.


75-104: LGTM! Comprehensive past meeting attachment interface.

The new interface properly represents past meeting attachments with complete metadata. Note the field naming difference (content_type vs mime_type in MeetingAttachment), which likely reflects the different backend API schemas for current vs. past meetings.

apps/lfx-one/src/app/shared/services/meeting.service.ts (2)

250-269: LGTM! Proper file attachment upload implementation.

The method correctly handles file upload to the V2 API with proper observable patterns (defer for lazy execution), error handling, and appropriate data structure including the type: 'file' discriminator.


478-491: LGTM! Clean helper function.

The readFileAsBase64 helper properly encapsulates file reading logic with appropriate error handling, making the createFileAttachment method cleaner and more maintainable.

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

56-64: LGTM! Clean controller delegation with proper route ordering.

The attachment routes are well-structured and properly delegate to controller methods. The route ordering is correct—the more specific /metadata route (line 60) is placed before the generic /:attachmentId route (line 62), preventing path matching conflicts.

public readonly existingAttachments = input<MeetingAttachment[]>([]);
public readonly isEditMode = input<boolean>(false);
public readonly deletingAttachmentId = input<string | null>(null);
public readonly meetingId = input<string | null>(null);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify meetingId is always provided when needed.

The new meetingId input is required for file uploads but is nullable. Ensure that file uploads are only initiated when meetingId is available.

Run the following script to verify meetingId usage:


🏁 Script executed:

#!/bin/bash
# Check where MeetingResourcesSummaryComponent is used and how meetingId is passed

ast-grep --pattern $'<lfx-meeting-resources-summary
  $$$
  [meetingId]=$_
  $$$
>'

Length of output: 0


I'll investigate the meetingId usage more thoroughly by searching for the component selector and examining the component implementation.


🏁 Script executed:

#!/bin/bash
# Search for lfx-meeting-resources-summary component usage in templates
rg -n 'lfx-meeting-resources-summary' --type=html --type=ts -A 3 -B 1

Length of output: 3906


🏁 Script executed:

#!/bin/bash
# Look at the component implementation to understand file upload logic
fd -e ts -e html -path '*/meeting-resources-summary/*' -type f

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Search for file upload handling in the component directory
rg -n 'upload\|file' 'apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/' -A 2 -B 2

Length of output: 0


Let me examine the component files directly and check the template more carefully:


🏁 Script executed:

#!/bin/bash
# Get all files in the meeting-resources-summary directory
fd . 'apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/' -type f

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Read the component TypeScript file
cat 'apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts'

Length of output: 10881


🏁 Script executed:

#!/bin/bash
# Read the component HTML template
cat 'apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.html'

Length of output: 9694


🏁 Script executed:

#!/bin/bash
# Check the full context of component usage in meeting-manage, especially around meetingId
rg -n 'meetingId' 'apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/' -B 3 -A 3

Length of output: 18969


Pass [meetingId] from parent to child component and implement proper null checks before file uploads.

The component uses this.meetingId()! (with non-null assertion) in the file upload handler (onFileSelect) and in the download link template, but the parent component never passes [meetingId] to this child component. In create mode, meetingId is null until the meeting is created, so file upload attempts will fail. Additionally, the component's template constructs a download URL using meetingId() without null safety.

Fix:

  1. In meeting-manage.component.html (lines 52 and 110), add [meetingId]="meetingId()" binding to both lfx-meeting-resources-summary uses
  2. In meeting-resources-summary.component.ts, replace the non-null assertion with a proper guard in onFileSelect():
    if (!this.meetingId()) {
      this.messageService.add({
        severity: 'error',
        summary: 'Upload Error',
        detail: 'Meeting must be created before uploading files',
        life: 5000,
      });
      return;
    }
  3. In the template, wrap the download link in a guard: @if (meetingId()) { ... }
🤖 Prompt for AI Agents
In
apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
around line 29 and related templates, the component currently uses
this.meetingId()! without safety and the parent never passes meetingId, causing
uploads/downloads to fail in create mode; to fix, 1) update the parent
meeting-manage.component.html at the two component usages (around lines 52 and
110) to add the binding [meetingId]="meetingId()", 2) in
meeting-resources-summary.component.ts replace the non-null assertion in
onFileSelect() with a null-check that shows an error toast and returns early if
!this.meetingId(), and 3) update the component template to conditionally render
the download link only when meetingId() is truthy (wrap link with an if guard).


// Start the upload
this.meetingService.uploadFileToStorage(file).subscribe({
this.meetingService.createFileAttachment(this.meetingId()!, file).subscribe({
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against null meetingId before file upload.

The non-null assertion operator (!) on this.meetingId() will throw if the value is null. Consider adding an explicit check before starting the upload.

Apply this diff to add proper null checking:

-        this.meetingService.createFileAttachment(this.meetingId()!, file).subscribe({
+        const meetingId = this.meetingId();
+        if (!meetingId) {
+          this.pendingAttachments.update((current) =>
+            current.map((pa) => (pa.id === pendingAttachment.id ? { ...pa, uploading: false, uploadError: 'Meeting ID is required' } : pa))
+          );
+          return;
+        }
+        this.meetingService.createFileAttachment(meetingId, file).subscribe({

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/lfx-one/src/app/modules/project/meetings/components/meeting-resources-summary/meeting-resources-summary.component.ts
around line 111, remove the non-null assertion on this.meetingId() and add an
explicit null check: capture const meetingId = this.meetingId(); if meetingId is
null/undefined, abort the upload (return early and optionally surface a
user-facing error/toast or disable the upload control); otherwise call
this.meetingService.createFileAttachment(meetingId, file).subscribe(...) so the
API is only invoked with a validated id.

Added binary request handling to ApiClientService and MicroserviceProxyService
to properly download file attachments without JSON parsing. Updated all
meeting attachment methods in MeetingService to use req.log instead of Logger
helper for consistent structured logging.

Changes:
- Add binaryRequest() method to ApiClientService for Buffer responses
- Add proxyBinaryRequest() method to MicroserviceProxyService
- Update getMeetingAttachment() to use microservice proxy
- Simplify custom headers logic in makeRequest()
- Replace Logger calls with req.log in all attachment methods:
  - createMeetingAttachment()
  - getMeetingAttachment()
  - deleteMeetingAttachment()
  - getMeetingAttachmentMetadata()
  - getMeetingAttachments()
  - getPastMeetingAttachments()

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

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
@andrest50 andrest50 force-pushed the andrest50/meeting-attachments branch from 5473052 to e0ddfa8 Compare November 11, 2025 21:19
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

♻️ Duplicate comments (2)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html (1)

111-119: Missing uploadSuccess event binding in single-page mode.

The lfx-meeting-resources-summary in EDIT MODE does not emit the uploadSuccess event handler. When a file is uploaded in single-page edit mode, newly uploaded attachments won't be added to the attachments list immediately without this binding. For consistency with the stepper mode and to ensure proper UX in edit mode, add the missing event binding:

                  <lfx-meeting-resources-summary
                    [form]="form()"
                    [meetingId]="meetingId()"
                    [existingAttachments]="attachments()"
                    [isEditMode]="isEditMode()"
                    [deletingAttachmentId]="deletingAttachmentId()"
                    (goToStep)="goToStep($event)"
                    (deleteAttachment)="deleteAttachment($event)"
+                   (uploadSuccess)="onAttachmentUploadSuccess($event)">
                  </lfx-meeting-resources-summary>
apps/lfx-one/src/server/controllers/meeting.controller.ts (1)

977-984: Validate file size to prevent DoS attacks.

The base64-encoded file is decoded into a Buffer without size validation, allowing arbitrarily large files to be uploaded. This can lead to memory exhaustion and denial-of-service attacks.

Apply this diff to add file size validation:

       // If file data is provided, include it (base64 encoded file from client)
       if (attachmentData.file) {
         const buffer = Buffer.from(attachmentData.file, 'base64');
+        
+        // Validate file size (e.g., 10MB limit)
+        const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
+        if (buffer.length > MAX_FILE_SIZE) {
+          const validationError = ServiceValidationError.forField(
+            'file',
+            `File size exceeds maximum of ${MAX_FILE_SIZE / 1024 / 1024}MB`,
+            {
+              operation: 'create_meeting_attachment',
+              service: 'meeting_controller',
+              path: req.path,
+            }
+          );
+          Logger.error(req, 'create_meeting_attachment', startTime, validationError);
+          return next(validationError);
+        }
+        
         formData.append('file', buffer, {
           filename: attachmentData.name,
           contentType: attachmentData.file_content_type || 'application/octet-stream',
         });
       }
🧹 Nitpick comments (6)
apps/lfx-one/src/server/services/api-client.service.ts (2)

121-143: Consider adding an Accept header for binary requests.

Unlike makeRequest which sets Accept: 'application/json', the binary request path doesn't set an Accept header. While this may be intentional, adding an explicit Accept header could improve clarity:

 private async makeBinaryRequest(method: string, url: string, bearerToken?: string, customHeaders?: Record<string, string>): Promise<ApiResponse<Buffer>> {
   const headers: Record<string, string> = {
     ['User-Agent']: 'LFX-PCC-Server/1.0',
+    Accept: '*/*',  // or 'application/octet-stream' for binary content
   };

207-268: Significant code duplication with executeRequest.

The executeBinaryRequest method duplicates approximately 90% of the logic from executeRequest (lines 145-205). The only meaningful difference is the response body handling (ArrayBuffer→Buffer vs text→JSON).

Consider extracting the shared error handling and request execution logic:

private async executeRequest<T>(url: string, requestInit: RequestInit): Promise<ApiResponse<T>> {
  return this.executeRequestWithParser(url, requestInit, async (response) => {
    const text = await response.text();
    return text ? JSON.parse(text) : null;
  }, 'api_client_request');
}

private async executeBinaryRequest(url: string, requestInit: RequestInit): Promise<ApiResponse<Buffer>> {
  return this.executeRequestWithParser(url, requestInit, async (response) => {
    const arrayBuffer = await response.arrayBuffer();
    return Buffer.from(arrayBuffer);
  }, 'api_client_binary_request');
}

private async executeRequestWithParser<T>(
  url: string,
  requestInit: RequestInit,
  parser: (response: Response) => Promise<T>,
  operation: string
): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url, requestInit);
    
    if (!response.ok) {
      // ... shared error handling ...
    }
    
    const data = await parser(response);
    
    return {
      data,
      status: response.status,
      statusText: response.statusText,
      headers: Object.fromEntries(response.headers.entries()),
    };
  } catch (error: unknown) {
    // ... shared error handling ...
  }
}

This would improve maintainability and ensure consistent error handling across both request types.

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

854-854: Consider stronger typing for attachmentData parameter.

The parameter is typed as any, but the comment on line 865 indicates it should be a FormData object. Consider using a more specific type or at minimum adding a type assertion/check for better type safety.

Apply this diff to improve type clarity:

-  public async createMeetingAttachment(req: Request, meetingUid: string, attachmentData: any): Promise<any> {
+  public async createMeetingAttachment(req: Request, meetingUid: string, attachmentData: unknown): Promise<any> {
apps/lfx-one/src/server/controllers/meeting.controller.ts (3)

952-969: Add validation for type field values.

The current validation only checks that type exists but doesn't verify it's either 'file' or 'link'. This allows invalid values to be proxied to the downstream API, resulting in unclear error messages.

Apply this diff to validate the type field:

       // Validate attachment data
       if (!attachmentData.type || !attachmentData.name) {
         const validationError = ServiceValidationError.fromFieldErrors(
           {
             type: !attachmentData.type ? 'Type is required' : [],
             name: !attachmentData.name ? 'Name is required' : [],
           },
           'Attachment data validation failed',
           {
             operation: 'create_meeting_attachment',
             service: 'meeting_controller',
             path: req.path,
           }
         );

         Logger.error(req, 'create_meeting_attachment', startTime, validationError);
         return next(validationError);
       }
+
+      // Validate type value
+      if (attachmentData.type !== 'file' && attachmentData.type !== 'link') {
+        const validationError = ServiceValidationError.forField(
+          'type',
+          'Type must be either "file" or "link"',
+          {
+            operation: 'create_meeting_attachment',
+            service: 'meeting_controller',
+            path: req.path,
+          }
+        );
+        Logger.error(req, 'create_meeting_attachment', startTime, validationError);
+        return next(validationError);
+      }

971-989: Add type-specific field validation.

The code doesn't validate that type: 'file' attachments have a file field or that type: 'link' attachments have a link field. This allows incomplete payloads to be proxied downstream, resulting in API errors instead of clear local validation errors.

Apply this diff after the type value validation:

+      // Validate type-specific fields
+      if (attachmentData.type === 'file' && !attachmentData.file) {
+        const validationError = ServiceValidationError.forField(
+          'file',
+          'File data is required for file-type attachments',
+          {
+            operation: 'create_meeting_attachment',
+            service: 'meeting_controller',
+            path: req.path,
+          }
+        );
+        Logger.error(req, 'create_meeting_attachment', startTime, validationError);
+        return next(validationError);
+      }
+
+      if (attachmentData.type === 'link' && !attachmentData.link) {
+        const validationError = ServiceValidationError.forField(
+          'link',
+          'Link URL is required for link-type attachments',
+          {
+            operation: 'create_meeting_attachment',
+            service: 'meeting_controller',
+            path: req.path,
+          }
+        );
+        Logger.error(req, 'create_meeting_attachment', startTime, validationError);
+        return next(validationError);
+      }
+
       // Create FormData for multipart/form-data request

1043-1061: Consider parallel metadata fetch or fix comment.

The comment on line 1047 states "fetch in parallel with file data," but the implementation is sequential: the file data is awaited on line 1045 before the metadata fetch begins on line 1052.

Either update the comment to reflect the sequential nature, or refactor to actually fetch in parallel:

Option 1: Fix the comment

-      // Get metadata to set proper filename (fetch in parallel with file data, but don't fail if metadata fails)
+      // Get metadata to set proper filename (best-effort, don't fail if metadata fetch fails)

Option 2: Make it truly parallel

-      // Get attachment file data via LFX V2 API (downloads file)
-      // The LFX V2 API returns the file with proper Content-Type and Content-Disposition headers
-      const attachmentData = await this.meetingService.getMeetingAttachment(req, uid, attachmentId);
-
-      // Get metadata to set proper filename (fetch in parallel with file data, but don't fail if metadata fails)
+      // Fetch file data and metadata in parallel
+      const [attachmentData, metadataResult] = await Promise.allSettled([
+        this.meetingService.getMeetingAttachment(req, uid, attachmentId),
+        this.meetingService.getMeetingAttachmentMetadata(req, uid, attachmentId)
+      ]);
+
+      if (attachmentData.status === 'rejected') {
+        throw attachmentData.reason;
+      }
+
+      const buffer = attachmentData.value;
       let filename = 'download';
       let contentType = 'application/octet-stream';

-      try {
-        const metadata = await this.meetingService.getMeetingAttachmentMetadata(req, uid, attachmentId);
+      if (metadataResult.status === 'fulfilled') {
+        const metadata = metadataResult.value;
         filename = metadata.name || filename;
         contentType = metadata.mime_type || metadata.content_type || contentType;
-      } catch (metadataError) {
+      } else {
         Logger.warning(req, 'get_meeting_attachment_metadata', 'Failed to fetch metadata, using defaults', {
           meeting_uid: uid,
           attachment_id: attachmentId,
-          error: metadataError instanceof Error ? metadataError.message : metadataError,
+          error: metadataResult.reason instanceof Error ? metadataResult.reason.message : metadataResult.reason,
         });
       }

Then update the variable name in subsequent uses from attachmentData to buffer.

📜 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 5473052 and e0ddfa8.

📒 Files selected for processing (6)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html (2 hunks)
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts (2 hunks)
  • apps/lfx-one/src/server/controllers/meeting.controller.ts (1 hunks)
  • apps/lfx-one/src/server/services/api-client.service.ts (3 hunks)
  • apps/lfx-one/src/server/services/meeting.service.ts (1 hunks)
  • apps/lfx-one/src/server/services/microservice-proxy.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/lfx-one/src/server/services/microservice-proxy.service.ts
  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-21T21:19:13.599Z
Learnt from: andrest50
Repo: linuxfoundation/lfx-v2-ui PR: 125
File: apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts:345-350
Timestamp: 2025-10-21T21:19:13.599Z
Learning: In the Angular meeting card component (apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts), when selecting between `summary.summary_data.edited_content` and `summary.summary_data.content`, the logical OR operator (`||`) is intentionally used instead of nullish coalescing (`??`) because empty string edited_content should fall back to the original content rather than being displayed as empty.

Applied to files:

  • apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html
🧬 Code graph analysis (3)
apps/lfx-one/src/server/services/api-client.service.ts (2)
packages/shared/src/interfaces/api.interface.ts (1)
  • ApiResponse (21-30)
apps/lfx-one/src/server/errors/index.ts (1)
  • MicroserviceError (6-6)
apps/lfx-one/src/server/controllers/meeting.controller.ts (1)
apps/lfx-one/src/server/errors/index.ts (1)
  • ServiceValidationError (7-7)
apps/lfx-one/src/server/services/meeting.service.ts (1)
packages/shared/src/interfaces/api.interface.ts (1)
  • QueryServiceResponse (58-61)
🔇 Additional comments (12)
apps/lfx-one/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.html (1)

54-54: ✓ Correctly pass meetingId to resources component for attachment operations.

Both instances of lfx-meeting-resources-summary now receive the meetingId() binding, enabling the resources component to perform create/read/delete attachment operations against the V2 API per the PR migration objectives.

Also applies to: 113-113

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

37-55: Verify if request body support is needed for binary requests.

The binaryRequest method signature includes POST, PUT, and PATCH methods but doesn't accept a data parameter. This prevents sending request bodies with these HTTP methods, which is inconsistent with the regular request() method.

Is this limitation intentional? If binary responses might be needed for POST/PUT/PATCH requests with payloads (e.g., uploading a file and receiving binary confirmation), consider adding a data parameter:

 public binaryRequest(
   type: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
   url: string,
   bearerToken?: string,
   query?: Record<string, any>,
+  data?: any,
   customHeaders?: Record<string, string>
 ): Promise<ApiResponse<Buffer>> {
   const fullUrl = this.getFullUrl(url, query);
-  return this.makeBinaryRequest(type, fullUrl, bearerToken, customHeaders);
+  return this.makeBinaryRequest(type, fullUrl, bearerToken, data, customHeaders);
 }

58-116: FormData handling looks solid.

The FormData detection using duck typing and conditional header handling is appropriate for the Node.js form-data package. The buffer conversion with Content-Length calculation correctly addresses Node.js fetch limitations.

Note: The error handling in the catch block (lines 104-111) was already flagged in a previous review regarding silent error swallowing.

apps/lfx-one/src/server/services/meeting.service.ts (5)

906-943: LGTM!

The binary file download implementation correctly uses proxyBinaryRequest to fetch the file as a Buffer and includes appropriate logging of the file size.


951-985: LGTM!

The delete operation follows the established pattern with proper error handling and logging.


987-1027: LGTM!

The metadata fetch implementation is straightforward and follows the service's established patterns.


1035-1083: LGTM!

The query-based attachment fetching follows the same pattern as other resource queries in this service (e.g., getMeetingRegistrants, getPastMeetingParticipants) with proper error handling and logging.


1091-1139: LGTM!

The past meeting attachments fetch mirrors the getMeetingAttachments implementation with the appropriate resource type and tags.

apps/lfx-one/src/server/controllers/meeting.controller.ts (4)

1093-1142: LGTM!

The delete attachment endpoint follows the controller's established patterns with proper validation and error handling.


1144-1193: LGTM!

The metadata endpoint correctly validates parameters and delegates to the service layer.


1198-1233: LGTM!

The get attachments list endpoint is implemented correctly with proper validation.


1238-1273: LGTM!

The past meeting attachments endpoint mirrors the meeting attachments implementation with appropriate validation.

@asithade asithade merged commit e860187 into main Nov 11, 2025
6 checks passed
@asithade asithade deleted the andrest50/meeting-attachments branch November 11, 2025 22:40
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