diff --git a/.changeset/add-update-meeting-tool.md b/.changeset/add-update-meeting-tool.md
new file mode 100644
index 0000000000..9710cb748f
--- /dev/null
+++ b/.changeset/add-update-meeting-tool.md
@@ -0,0 +1,13 @@
+---
+---
+
+feat: Add update_meeting tool to Addie
+
+Added a new tool that allows updating existing meetings through conversation:
+- Change title, description, or agenda
+- Reschedule to a new time (properly handles timezone conversion)
+- Update duration
+
+Updates are synchronized to Zoom and Google Calendar when those integrations
+are configured. This enables fixing meeting times or details without having
+to cancel and recreate.
diff --git a/.changeset/dirty-hoops-eat.md b/.changeset/dirty-hoops-eat.md
new file mode 100644
index 0000000000..a845151cc8
--- /dev/null
+++ b/.changeset/dirty-hoops-eat.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/.changeset/fix-meetings-section-display.md b/.changeset/fix-meetings-section-display.md
new file mode 100644
index 0000000000..68ca5f8610
--- /dev/null
+++ b/.changeset/fix-meetings-section-display.md
@@ -0,0 +1,11 @@
+---
+---
+
+fix: Display meetings section on committee pages and add admin edit
+
+Fixed a bug where the meetings section wasn't showing on committee detail pages.
+The API returns `{ meetings: [...] }` but the code was calling `.filter()` directly
+on the response object instead of `response.meetings`.
+
+Also added edit functionality to the admin meetings page - admins can now click
+"Edit" to reschedule or update meeting details (title, description, time).
diff --git a/server/public/admin-meetings.html b/server/public/admin-meetings.html
index f6aa4e2009..88c10ad2f3 100644
--- a/server/public/admin-meetings.html
+++ b/server/public/admin-meetings.html
@@ -396,6 +396,7 @@
Meeting Details
Close
+ Edit
Cancel Meeting
@@ -633,56 +634,112 @@ ${escapeHtml(s.title)}
document.getElementById('meetingModal').style.display = 'flex';
}
+ // Edit existing meeting
+ function editMeeting() {
+ if (!viewingMeeting) return;
+
+ const m = viewingMeeting;
+ const date = new Date(m.start_time);
+ const tz = m.timezone || 'America/New_York';
+
+ // Format date/time in the meeting's timezone for editing
+ const dateFormatter = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' });
+ const timeFormatter = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false });
+ const formattedDate = dateFormatter.format(date); // YYYY-MM-DD format
+ const formattedTime = timeFormatter.format(date).replace(/^24:/, '00:'); // HH:MM format
+
+ document.getElementById('modalTitle').textContent = 'Edit Meeting';
+ document.getElementById('meetingId').value = m.id;
+ document.getElementById('workingGroupId').value = m.working_group_id;
+ document.getElementById('title').value = m.title;
+ document.getElementById('meetingDate').value = formattedDate;
+ document.getElementById('meetingTime').value = formattedTime;
+ document.getElementById('timezone').value = tz;
+ document.getElementById('description').value = m.description || m.agenda || '';
+
+ // Calculate duration from start_time and end_time
+ if (m.end_time) {
+ const endDate = new Date(m.end_time);
+ const durationMs = endDate.getTime() - date.getTime();
+ document.getElementById('duration').value = Math.round(durationMs / 60000);
+ } else {
+ document.getElementById('duration').value = 60;
+ }
+
+ // Hide invite mode for edits (can't change invites after creation)
+ document.getElementById('inviteMode').parentElement.style.display = 'none';
+
+ document.getElementById('saveBtn').textContent = 'Save Changes';
+ closeDetailsModal();
+ document.getElementById('meetingModal').style.display = 'flex';
+ }
+
function closeModal() {
document.getElementById('meetingModal').style.display = 'none';
+ // Reset invite mode visibility
+ document.getElementById('inviteMode').parentElement.style.display = 'block';
+ document.getElementById('saveBtn').textContent = 'Schedule';
}
- // Save meeting
+ // Save meeting (create or update)
async function saveMeeting(event) {
event.preventDefault();
+ const meetingId = document.getElementById('meetingId').value;
+ const isUpdate = !!meetingId;
+
const date = document.getElementById('meetingDate').value;
const time = document.getElementById('meetingTime').value;
const timezone = document.getElementById('timezone').value;
+ const durationMinutes = parseInt(document.getElementById('duration').value) || 60;
// Construct ISO datetime
const startTime = new Date(`${date}T${time}`);
+ const endTime = new Date(startTime.getTime() + durationMinutes * 60000);
const data = {
- working_group_id: document.getElementById('workingGroupId').value,
title: document.getElementById('title').value.trim(),
description: document.getElementById('description').value.trim() || null,
start_time: startTime.toISOString(),
- duration_minutes: parseInt(document.getElementById('duration').value) || 60,
+ end_time: endTime.toISOString(),
timezone: timezone,
- invite_mode: document.getElementById('inviteMode').value,
};
+ // Only include these fields for new meetings
+ if (!isUpdate) {
+ data.working_group_id = document.getElementById('workingGroupId').value;
+ data.duration_minutes = durationMinutes;
+ data.invite_mode = document.getElementById('inviteMode').value;
+ }
+
const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
- saveBtn.textContent = 'Scheduling...';
+ saveBtn.textContent = isUpdate ? 'Saving...' : 'Scheduling...';
try {
- const response = await fetch('/api/admin/meetings', {
- method: 'POST',
+ const url = isUpdate ? `/api/admin/meetings/${meetingId}` : '/api/admin/meetings';
+ const method = isUpdate ? 'PUT' : 'POST';
+
+ const response = await fetch(url, {
+ method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
- throw new Error(error.message || 'Failed to schedule meeting');
+ throw new Error(error.message || `Failed to ${isUpdate ? 'update' : 'schedule'} meeting`);
}
closeModal();
loadMeetings();
} catch (error) {
- console.error('Error scheduling meeting:', error);
- alert('Failed to schedule meeting: ' + error.message);
+ console.error(`Error ${isUpdate ? 'updating' : 'scheduling'} meeting:`, error);
+ alert(`Failed to ${isUpdate ? 'update' : 'schedule'} meeting: ` + error.message);
}
saveBtn.disabled = false;
- saveBtn.textContent = 'Schedule';
+ saveBtn.textContent = isUpdate ? 'Save Changes' : 'Schedule';
}
// View meeting details
@@ -731,7 +788,10 @@ ${escapeHtml(s.title)}
`;
- document.getElementById('cancelMeetingBtn').style.display = m.status === 'scheduled' ? 'block' : 'none';
+ // Only show Edit and Cancel buttons for scheduled meetings
+ const isScheduled = m.status === 'scheduled';
+ document.getElementById('editMeetingBtn').style.display = isScheduled ? 'inline-block' : 'none';
+ document.getElementById('cancelMeetingBtn').style.display = isScheduled ? 'inline-block' : 'none';
document.getElementById('detailsModal').style.display = 'flex';
} catch (error) {
console.error('Error loading meeting:', error);
diff --git a/server/public/working-groups/detail.html b/server/public/working-groups/detail.html
index 2bc76e2327..a263930244 100644
--- a/server/public/working-groups/detail.html
+++ b/server/public/working-groups/detail.html
@@ -1674,10 +1674,10 @@ Recent Events
const response = await fetch(`/api/meetings?working_group_id=${currentGroup.id}`);
if (!response.ok) return;
- const meetings = await response.json();
- // Filter to only upcoming meetings
+ const data = await response.json();
+ // API returns { meetings: [...] } - filter to scheduled upcoming meetings
const now = new Date();
- const upcoming = meetings.filter(m => new Date(m.start_time) > now && m.status === 'scheduled');
+ const upcoming = (data.meetings || []).filter(m => new Date(m.start_time) > now && m.status === 'scheduled');
const meetingsSection = document.getElementById('meetingsSection');
const meetingsList = document.getElementById('meetingsList');
diff --git a/server/src/addie/mcp/meeting-tools.ts b/server/src/addie/mcp/meeting-tools.ts
index c4046fbc72..ae3915d1db 100644
--- a/server/src/addie/mcp/meeting-tools.ts
+++ b/server/src/addie/mcp/meeting-tools.ts
@@ -384,6 +384,48 @@ Example prompts this handles:
required: ['meeting_id'],
},
},
+ {
+ name: 'update_meeting',
+ description: `Update an existing meeting's details. Use this when someone wants to change the time, title, description, or agenda of a scheduled meeting.
+
+This will update the meeting in the database, Zoom (if configured), and Google Calendar.
+
+IMPORTANT: For start_time, provide the time in the user's timezone WITHOUT a Z suffix (same as schedule_meeting).`,
+ input_schema: {
+ type: 'object' as const,
+ properties: {
+ meeting_id: {
+ type: 'string',
+ description: 'Meeting ID (UUID or Zoom meeting ID)',
+ },
+ title: {
+ type: 'string',
+ description: 'New meeting title',
+ },
+ description: {
+ type: 'string',
+ description: 'New meeting description',
+ },
+ agenda: {
+ type: 'string',
+ description: 'New meeting agenda (markdown supported)',
+ },
+ start_time: {
+ type: 'string',
+ description: 'New start time in ISO 8601 format WITHOUT timezone suffix (e.g., "2026-01-15T14:00:00" for 2pm). Use timezone parameter to specify the timezone.',
+ },
+ duration_minutes: {
+ type: 'number',
+ description: 'New duration in minutes',
+ },
+ timezone: {
+ type: 'string',
+ description: 'Timezone for start_time (default: America/New_York)',
+ },
+ },
+ required: ['meeting_id'],
+ },
+ },
{
name: 'add_meeting_attendee',
description: `Add someone to a meeting. Use this when someone asks to add a specific person to a scheduled meeting.`,
@@ -929,6 +971,165 @@ export function createMeetingToolHandlers(
}
});
+ // Update meeting
+ handlers.set('update_meeting', async (input) => {
+ const permCheck = await checkSchedulePermission();
+ if (permCheck) return permCheck;
+
+ const meetingId = input.meeting_id as string;
+
+ // Try to find by our UUID first, then by Zoom meeting ID
+ let meeting = await meetingsDb.getMeetingById(meetingId);
+ if (!meeting) {
+ meeting = await meetingsDb.getMeetingByZoomId(meetingId);
+ }
+ if (!meeting) {
+ return `❌ Meeting not found: "${meetingId}". Use list_upcoming_meetings to find the correct meeting ID.`;
+ }
+
+ if (meeting.status === 'cancelled') {
+ return `❌ Cannot update a cancelled meeting.`;
+ }
+
+ // Build updates object
+ const updates: Record = {};
+ const changes: string[] = [];
+
+ if (input.title) {
+ updates.title = input.title as string;
+ changes.push(`Title → "${input.title}"`);
+ }
+
+ if (input.description !== undefined) {
+ updates.description = input.description as string;
+ changes.push('Description updated');
+ }
+
+ if (input.agenda !== undefined) {
+ updates.agenda = input.agenda as string;
+ changes.push('Agenda updated');
+ }
+
+ // Handle time updates
+ const startTimeStr = input.start_time as string | undefined;
+ const durationMinutes = input.duration_minutes as number | undefined;
+ const timezone = (input.timezone as string) || meeting.timezone || 'America/New_York';
+
+ // Calculate current duration from start_time and end_time
+ const currentDuration = meeting.start_time && meeting.end_time
+ ? Math.round((meeting.end_time.getTime() - meeting.start_time.getTime()) / 60000)
+ : 60;
+
+ if (startTimeStr) {
+ const startTime = parseDateInTimezone(startTimeStr, timezone);
+ if (isNaN(startTime.getTime())) {
+ return `❌ Invalid start time format. Please use ISO 8601 format (e.g., "2026-01-15T14:00:00").`;
+ }
+ updates.start_time = startTime;
+ updates.timezone = timezone;
+
+ const duration = durationMinutes || currentDuration;
+ updates.end_time = new Date(startTime.getTime() + duration * 60 * 1000);
+
+ changes.push(`Time → ${formatDate(startTime)} at ${formatTime(startTime, timezone)}`);
+ } else if (durationMinutes && meeting.start_time) {
+ // Just updating duration, keep existing start time
+ updates.end_time = new Date(meeting.start_time.getTime() + durationMinutes * 60 * 1000);
+ changes.push(`Duration → ${durationMinutes} minutes`);
+ }
+
+ if (changes.length === 0) {
+ return `No changes specified. You can update: title, description, agenda, start_time, duration_minutes, timezone.`;
+ }
+
+ try {
+ // Update in database
+ const updatedMeeting = await meetingsDb.updateMeeting(meeting.id, updates);
+ if (!updatedMeeting) {
+ return `❌ Failed to update meeting in database.`;
+ }
+
+ const errors: string[] = [];
+
+ // Update Zoom meeting if time changed and Zoom meeting exists
+ if (updates.start_time && meeting.zoom_meeting_id && zoom.isZoomConfigured()) {
+ try {
+ const startTime = updates.start_time as Date;
+ await zoom.updateMeeting(meeting.zoom_meeting_id, {
+ start_time: startTime.toISOString(),
+ duration: durationMinutes || currentDuration,
+ timezone,
+ });
+ } catch (error) {
+ const msg = error instanceof Error ? error.message : 'Unknown error';
+ errors.push(`Zoom update failed: ${msg}`);
+ logger.error({ err: error, meetingId: meeting.id }, 'Failed to update Zoom meeting');
+ }
+ }
+
+ // Update Google Calendar event if it exists
+ if (meeting.google_calendar_event_id && calendar.isGoogleCalendarConfigured()) {
+ try {
+ const startTime = (updates.start_time as Date) || meeting.start_time;
+ const title = (updates.title as string) || meeting.title;
+
+ // Calculate end time - use updated value, or compute from start + duration
+ let endTime = updates.end_time as Date | undefined;
+ if (!endTime) {
+ if (meeting.end_time) {
+ endTime = meeting.end_time;
+ } else {
+ // Fallback: compute from start time + current duration
+ endTime = new Date(startTime.getTime() + currentDuration * 60 * 1000);
+ }
+ }
+
+ // Get working group for calendar event summary
+ const workingGroup = await workingGroupDb.getWorkingGroupById(meeting.working_group_id);
+ const summary = workingGroup ? `${workingGroup.name}: ${title}` : title;
+
+ await calendar.updateEvent(meeting.google_calendar_event_id, {
+ summary,
+ description: (updates.description as string) || meeting.description || undefined,
+ start: {
+ dateTime: startTime.toISOString(),
+ timeZone: timezone,
+ },
+ end: {
+ dateTime: endTime.toISOString(),
+ timeZone: timezone,
+ },
+ });
+ } catch (error) {
+ const msg = error instanceof Error ? error.message : 'Unknown error';
+ errors.push(`Calendar update failed: ${msg}`);
+ logger.error({ err: error, meetingId: meeting.id }, 'Failed to update calendar event');
+ }
+ }
+
+ let response = `✅ Updated: **${updatedMeeting.title}**\n\n`;
+ response += `**Changes:**\n${changes.map(c => `• ${c}`).join('\n')}\n`;
+
+ if (errors.length > 0) {
+ response += `\n⚠️ Some integrations had issues:\n${errors.map(e => `• ${e}`).join('\n')}`;
+ } else if (meeting.google_calendar_event_id) {
+ response += `\n📧 Calendar invites have been updated.`;
+ }
+
+ logger.info({
+ meetingId: meeting.id,
+ changes,
+ updatedBy: getUserId(),
+ }, 'Meeting updated via Addie');
+
+ return response;
+ } catch (error) {
+ const msg = error instanceof Error ? error.message : 'Unknown error';
+ logger.error({ err: error, meetingId: meeting.id }, 'Failed to update meeting via Addie');
+ return `❌ Failed to update meeting: ${msg}`;
+ }
+ });
+
// Add attendee
handlers.set('add_meeting_attendee', async (input) => {
const permCheck = await checkSchedulePermission();