diff --git a/CLAUDE.md b/CLAUDE.md index cd63d8a6..519f562c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,3 +134,5 @@ Before starting any work or commits: 2. **Create JIRA ticket if needed** for untracked work 3. **Include JIRA ticket in commit message** (e.g., LFXV2-XXX) 4. **Link PR to JIRA ticket** when creating pull requests + +- Always use sequential thinking mcp for planning before doing any changes diff --git a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts index 9c25df9f..bba7b2ab 100644 --- a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts +++ b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-manage/meeting-manage.component.ts @@ -228,7 +228,7 @@ export class MeetingManageComponent { this.messageService.add({ severity: 'error', summary: 'Error', - detail: 'Project information is required to create a meeting.', + detail: `Project information is required to ${this.isEditMode() ? 'update' : 'create'} a meeting.`, }); return; } @@ -401,6 +401,13 @@ export class MeetingManageComponent { // Map recurrence object back to form value const recurrenceValue = mapRecurrenceToFormValue(meeting.recurrence); + // If recording_enabled is true, enable controls for transcript_enabled and youtube_upload_enabled + if (meeting.recording_enabled) { + this.form().get('transcript_enabled')?.enable(); + this.form().get('youtube_upload_enabled')?.enable(); + this.form().get('zoom_ai_enabled')?.enable(); + } + this.form().patchValue({ title: meeting.title, description: meeting.description, diff --git a/apps/lfx-pcc/src/server/controllers/meeting.controller.ts b/apps/lfx-pcc/src/server/controllers/meeting.controller.ts index bd23a5f8..6e661f40 100644 --- a/apps/lfx-pcc/src/server/controllers/meeting.controller.ts +++ b/apps/lfx-pcc/src/server/controllers/meeting.controller.ts @@ -1,7 +1,7 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -import { CreateMeetingRequest } from '@lfx-pcc/shared/interfaces'; +import { CreateMeetingRequest, UpdateMeetingRequest } from '@lfx-pcc/shared/interfaces'; import { Request, Response } from 'express'; import { Logger } from '../helpers/logger'; @@ -134,6 +134,95 @@ export class MeetingController { } } + /** + * PUT /meetings/:id + */ + public async updateMeeting(req: Request, res: Response): Promise { + const { id } = req.params; + const meetingData: UpdateMeetingRequest = req.body; + const { editType } = req.query; + const startTime = Logger.start(req, 'update_meeting', { + meeting_id: id, + project_uid: meetingData?.project_uid, + title: meetingData?.title, + start_time: meetingData?.start_time, + duration: meetingData?.duration, + timezone: meetingData?.timezone, + edit_type: editType, + body_size: JSON.stringify(req.body).length, + }); + + try { + if (!id) { + Logger.error(req, 'update_meeting', startTime, new Error('Missing meeting ID parameter')); + + Responder.badRequest(res, 'Meeting ID is required', { + code: 'MISSING_MEETING_ID', + }); + return; + } + + // Basic validation + if (!meetingData.title || !meetingData.start_time || !meetingData.project_uid || !meetingData.duration || !meetingData.timezone) { + Logger.error(req, 'update_meeting', startTime, new Error('Missing required fields for meeting update'), { + provided_fields: { + has_title: !!meetingData.title, + has_start_time: !!meetingData.start_time, + has_project_uid: !!meetingData.project_uid, + has_duration: !!meetingData.duration, + has_timezone: !!meetingData.timezone, + }, + }); + + Responder.badRequest(res, 'Title, start_time, duration, timezone, and project_uid are required fields', { + code: 'MISSING_REQUIRED_FIELDS', + }); + return; + } + + // Validate duration range + if (meetingData.duration < 0 || meetingData.duration > 600) { + Logger.error(req, 'update_meeting', startTime, new Error('Invalid duration for meeting update'), { + provided_duration: meetingData.duration, + }); + + Responder.badRequest(res, 'Duration must be between 0 and 600 minutes', { + code: 'INVALID_DURATION', + }); + return; + } + + // Validate editType for recurring meetings + if (editType && !['single', 'future'].includes(editType as string)) { + Logger.error(req, 'update_meeting', startTime, new Error('Invalid edit type for meeting update'), { + provided_edit_type: editType, + }); + + Responder.badRequest(res, 'Edit type must be "single" or "future"', { + code: 'INVALID_EDIT_TYPE', + }); + return; + } + + const meeting = await this.meetingService.updateMeeting(req, id, meetingData, editType as 'single' | 'future'); + + Logger.success(req, 'update_meeting', startTime, { + meeting_id: id, + project_uid: meeting.project_uid, + title: meeting.title, + edit_type: editType || 'single', + }); + + res.json(meeting); + } catch (error) { + Logger.error(req, 'update_meeting', startTime, error, { + meeting_id: id, + edit_type: editType, + }); + Responder.handle(res, error, 'update_meeting'); + } + } + /** * DELETE /meetings/:id */ diff --git a/apps/lfx-pcc/src/server/routes/meetings.ts b/apps/lfx-pcc/src/server/routes/meetings.ts index 930348ff..9eb0ccf5 100644 --- a/apps/lfx-pcc/src/server/routes/meetings.ts +++ b/apps/lfx-pcc/src/server/routes/meetings.ts @@ -168,46 +168,8 @@ router.get('/:id', (req, res) => meetingController.getMeetingById(req, res)); // POST /meetings - using new controller pattern router.post('/', (req, res) => meetingController.createMeeting(req, res)); -router.put('/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const meetingId = req.params['id']; - const meetingData = req.body; - const { editType } = req.query; - - if (!meetingId) { - return res.status(400).json({ - error: 'Meeting ID is required', - code: 'MISSING_MEETING_ID', - }); - } - - // Validate editType for recurring meetings - if (editType && !['single', 'future'].includes(editType as string)) { - return res.status(400).json({ - error: 'Edit type must be "single" or "future"', - code: 'INVALID_EDIT_TYPE', - }); - } - - // Remove fields that shouldn't be updated directly - delete meetingData.id; - delete meetingData.created_at; - - const meeting = await supabaseService.updateMeeting(meetingId, meetingData, editType as 'single' | 'future'); - - return res.json(meeting); - } catch (error) { - req.log.error( - { - error: error instanceof Error ? error.message : error, - meeting_id: req.params['id'], - edit_type: req.query['editType'], - }, - 'Failed to update meeting' - ); - return next(error); - } -}); +// PUT /meetings/:id - using new controller pattern +router.put('/:id', (req, res) => meetingController.updateMeeting(req, res)); router.delete('/:id', (req, res) => meetingController.deleteMeeting(req, res)); diff --git a/apps/lfx-pcc/src/server/services/etag.service.ts b/apps/lfx-pcc/src/server/services/etag.service.ts index 19d9aac8..d8e28751 100644 --- a/apps/lfx-pcc/src/server/services/etag.service.ts +++ b/apps/lfx-pcc/src/server/services/etag.service.ts @@ -1,8 +1,8 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -import { ETagError, ETagResult, extractErrorDetails } from '@lfx-pcc/shared/interfaces'; import { HTTP_HEADERS } from '@lfx-pcc/shared/constants'; +import { ETagError, ETagResult, extractErrorDetails } from '@lfx-pcc/shared/interfaces'; import { Request } from 'express'; import { MicroserviceProxyService } from './microservice-proxy.service'; diff --git a/apps/lfx-pcc/src/server/services/meeting.service.ts b/apps/lfx-pcc/src/server/services/meeting.service.ts index 68baf7a0..06f29eb1 100644 --- a/apps/lfx-pcc/src/server/services/meeting.service.ts +++ b/apps/lfx-pcc/src/server/services/meeting.service.ts @@ -1,11 +1,11 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -import { CreateMeetingRequest, ETagError, Meeting, QueryServiceResponse } from '@lfx-pcc/shared/interfaces'; +import { CreateMeetingRequest, ETagError, Meeting, QueryServiceResponse, UpdateMeetingRequest } from '@lfx-pcc/shared/interfaces'; import { Request } from 'express'; -import { getUsernameFromAuth } from '../utils/auth-helper'; import { Logger } from '../helpers/logger'; +import { getUsernameFromAuth } from '../utils/auth-helper'; import { ApiClientService } from './api-client.service'; import { ETagService } from './etag.service'; import { MicroserviceProxyService } from './microservice-proxy.service'; @@ -101,6 +101,57 @@ export class MeetingService { return newMeeting; } + /** + * Updates a meeting using ETag for concurrency control + */ + public async updateMeeting(req: Request, meetingId: string, meetingData: UpdateMeetingRequest, editType?: 'single' | 'future'): Promise { + // Step 1: Fetch meeting with ETag + const { etag, data } = await this.etagService.fetchWithETag(req, 'LFX_V2_SERVICE', `/meetings/${meetingId}`, 'update_meeting'); + + // Get the logged-in user's username to maintain organizer if not provided + const username = await getUsernameFromAuth(req); + + // Create organizers array ensuring no duplicates or null values + const existingOrganizers = data.organizers || []; + const organizersSet = new Set(existingOrganizers.filter((organizer) => organizer != null)); + + // Add current user as organizer if username exists and not already included + if (username) { + organizersSet.add(username); + } + + // Include organizers in the update payload + const updatePayload = { + ...meetingData, + organizers: Array.from(organizersSet), + }; + + const sanitizedPayload = Logger.sanitize({ updatePayload, editType }); + req.log.info(sanitizedPayload, 'Updating meeting payload'); + + // Step 2: Update meeting with ETag, including editType query parameter if provided + let path = `/meetings/${meetingId}`; + if (editType) { + path += `?editType=${editType}`; + } + + const updatedMeeting = await this.etagService.updateWithETag(req, 'LFX_V2_SERVICE', path, etag, updatePayload, 'update_meeting'); + + req.log.info( + { + operation: 'update_meeting', + meeting_id: meetingId, + project_uid: updatedMeeting.project_uid, + title: updatedMeeting.title, + edit_type: editType || 'single', + organizer: username || 'none', + }, + 'Meeting updated successfully' + ); + + return updatedMeeting; + } + /** * Deletes a meeting using ETag for concurrency control */