diff --git a/server/migrations/versions/a256772ef058_add_ics_uid_to_calendar_event.py b/server/migrations/versions/a256772ef058_add_ics_uid_to_calendar_event.py deleted file mode 100644 index 06dce024..00000000 --- a/server/migrations/versions/a256772ef058_add_ics_uid_to_calendar_event.py +++ /dev/null @@ -1,46 +0,0 @@ -"""add_ics_uid_to_calendar_event - -Revision ID: a256772ef058 -Revises: d4a1c446458c -Create Date: 2025-08-19 09:27:26.472456 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "a256772ef058" -down_revision: Union[str, None] = "d4a1c446458c" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("calendar_event", schema=None) as batch_op: - batch_op.add_column(sa.Column("ics_uid", sa.Text(), nullable=False)) - batch_op.drop_constraint(batch_op.f("uq_room_calendar_event"), type_="unique") - batch_op.create_unique_constraint( - "uq_room_calendar_event", ["room_id", "ics_uid"] - ) - batch_op.drop_column("external_id") - - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("calendar_event", schema=None) as batch_op: - batch_op.add_column( - sa.Column("external_id", sa.TEXT(), autoincrement=False, nullable=True) - ) - batch_op.drop_constraint("uq_room_calendar_event", type_="unique") - batch_op.create_unique_constraint( - batch_op.f("uq_room_calendar_event"), ["room_id", "external_id"] - ) - batch_op.drop_column("ics_uid") - - # ### end Alembic commands ### diff --git a/www/app/(app)/rooms/[roomName]/calendar/page.tsx b/www/app/(app)/rooms/[roomName]/calendar/page.tsx index af3fc0fd..64657022 100644 --- a/www/app/(app)/rooms/[roomName]/calendar/page.tsx +++ b/www/app/(app)/rooms/[roomName]/calendar/page.tsx @@ -5,69 +5,60 @@ import { VStack, Heading, Text, - Card, HStack, Badge, Spinner, Flex, Link, Button, - Alert, IconButton, Tooltip, Wrap, } from "@chakra-ui/react"; import { useParams, useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { FaSync, FaClock, FaUsers, FaEnvelope } from "react-icons/fa"; import { LuArrowLeft } from "react-icons/lu"; -import useApi from "../../../../lib/useApi"; -import { CalendarEventResponse } from "../../../../api"; +import { + useRoomCalendarEvents, + useRoomIcsSync, +} from "../../../../lib/apiHooks"; +import type { components } from "../../../../reflector-api"; + +type CalendarEventResponse = components["schemas"]["CalendarEventResponse"]; export default function RoomCalendarPage() { const params = useParams(); const router = useRouter(); const roomName = params.roomName as string; - const api = useApi(); - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const [syncing, setSyncing] = useState(false); - const fetchEvents = async () => { - if (!api) return; + // React Query hooks + const eventsQuery = useRoomCalendarEvents(roomName); + const syncMutation = useRoomIcsSync(); - try { - setLoading(true); - setError(null); - const response = await api.v1RoomsListMeetings({ roomName }); - setEvents(response); - } catch (err: any) { - setError(err.body?.detail || "Failed to load calendar events"); - } finally { - setLoading(false); - } - }; + const events = eventsQuery.data || []; + const loading = eventsQuery.isLoading; + const error = eventsQuery.error ? "Failed to load calendar events" : null; const handleSync = async () => { - if (!api) return; - try { setSyncing(true); - await api.v1RoomsSyncIcs({ roomName }); - await fetchEvents(); // Refresh events after sync + await syncMutation.mutateAsync({ + params: { + path: { room_name: roomName }, + }, + }); + // Refetch events after sync + await eventsQuery.refetch(); } catch (err: any) { - setError(err.body?.detail || "Failed to sync calendar"); + console.error("Sync failed:", err); } finally { setSyncing(false); } }; - useEffect(() => { - fetchEvents(); - }, [api, roomName]); - const formatEventTime = (start: string, end: string) => { const startDate = new Date(start); const endDate = new Date(end); @@ -125,7 +116,7 @@ export default function RoomCalendarPage() { Attendees: - + {attendees.map((attendee, index) => { const email = getAttendeeEmail(attendee); const display = getAttendeeDisplay(attendee); @@ -178,9 +169,9 @@ export default function RoomCalendarPage() { return ( - + - + Calendar for {roomName} - {error && ( - - - {error} - + + + Error + + {error} + )} {loading ? ( @@ -214,66 +209,62 @@ export default function RoomCalendarPage() { ) : events.length === 0 ? ( - - - - No calendar events found. Make sure your calendar is configured - and synced. - - - + + + No calendar events found. Make sure your calendar is configured + and synced. + + ) : ( - + {/* Active Events */} {activeEvents.length > 0 && ( Active Now - + {activeEvents.map((event) => ( - - - - - - - {event.title || "Untitled Event"} - - Active - - - - - {formatEventTime( - event.start_time, - event.end_time, - )} - - - {event.description && ( - - {event.description} - - )} - {renderAttendees(event.attendees)} - - - - - - - + + + + + {event.title || "Untitled Event"} + + Active + + + + + {formatEventTime( + event.start_time, + event.end_time, + )} + + + {event.description && ( + + {event.description} + + )} + {renderAttendees(event.attendees)} + + + + + + ))} diff --git a/www/app/(app)/rooms/_components/ICSSettings.tsx b/www/app/(app)/rooms/_components/ICSSettings.tsx index 1d46cc95..cccd3edc 100644 --- a/www/app/(app)/rooms/_components/ICSSettings.tsx +++ b/www/app/(app)/rooms/_components/ICSSettings.tsx @@ -143,11 +143,7 @@ export default function ICSSettings({ } return ( - - - Calendar Integration (ICS) - - + - - Room name - - - No spaces or special characters allowed - - {nameError && {nameError}} - + + + General + Calendar + Share + - - { - const syntheticEvent = { - target: { - name: "isLocked", - type: "checkbox", - checked: e.checked, - }, - }; - handleRoomChange(syntheticEvent); - }} - > - - - - - Locked room - - - - Room size - - setRoomInput({ ...room, roomMode: e.value[0] }) - } - collection={roomModeCollection} - > - - - - - - - - - - - - {roomModeOptions.map((option) => ( - - {option.label} - - - ))} - - - - - - Recording type - - setRoomInput({ - ...room, - recordingType: e.value[0], - recordingTrigger: - e.value[0] !== "cloud" ? "none" : room.recordingTrigger, - }) - } - collection={recordingTypeCollection} - > - - - - - - - - - - - - {recordingTypeOptions.map((option) => ( - - {option.label} - - - ))} - - - - - - Cloud recording start trigger - - setRoomInput({ ...room, recordingTrigger: e.value[0] }) - } - collection={recordingTriggerCollection} - disabled={room.recordingType !== "cloud"} - > - - - - - - - - - - - - {recordingTriggerOptions.map((option) => ( - - {option.label} - - - ))} - - - - - - { - const syntheticEvent = { - target: { - name: "zulipAutoPost", - type: "checkbox", - checked: e.checked, - }, - }; - handleRoomChange(syntheticEvent); - }} - > - - - - - - Automatically post transcription to Zulip - - - - - Zulip stream - - setRoomInput({ - ...room, - zulipStream: e.value[0], - zulipTopic: "", - }) - } - collection={streamCollection} - disabled={!room.zulipAutoPost} - > - - - - - - - - - - - - {streamOptions.map((option) => ( - - {option.label} - - - ))} - - - - - - Zulip topic - - setRoomInput({ ...room, zulipTopic: e.value[0] }) - } - collection={topicCollection} - disabled={!room.zulipAutoPost} - > - - - - - - - - - - - - {topicOptions.map((option) => ( - - {option.label} - - - ))} - - - - - - {/* Webhook Configuration Section */} - - Webhook URL - - - Optional: URL to receive notifications when transcripts are - ready - - - - {room.webhookUrl && ( - <> - - Webhook Secret - - - {isEditing && room.webhookSecret && ( - - setShowWebhookSecret(!showWebhookSecret) - } - > - {showWebhookSecret ? : } - - )} - + + + Room name + - Used for HMAC signature verification (auto-generated if - left empty) + No spaces or special characters allowed + + {nameError && ( + {nameError} + )} + + + + { + const syntheticEvent = { + target: { + name: "isLocked", + type: "checkbox", + checked: e.checked, + }, + }; + handleRoomChange(syntheticEvent); + }} + > + + + + + Locked room + + + + + Room size + + setRoomInput({ ...room, roomMode: e.value[0] }) + } + collection={roomModeCollection} + > + + + + + + + + + + + + {roomModeOptions.map((option) => ( + + {option.label} + + + ))} + + + + + + + Recording type + + setRoomInput({ + ...room, + recordingType: e.value[0], + recordingTrigger: + e.value[0] !== "cloud" + ? "none" + : room.recordingTrigger, + }) + } + collection={recordingTypeCollection} + > + + + + + + + + + + + + {recordingTypeOptions.map((option) => ( + + {option.label} + + + ))} + + + + + + + Cloud recording start trigger + + setRoomInput({ ...room, recordingTrigger: e.value[0] }) + } + collection={recordingTriggerCollection} + disabled={room.recordingType !== "cloud"} + > + + + + + + + + + + + + {recordingTriggerOptions.map((option) => ( + + {option.label} + + + ))} + + + + + + + { + const syntheticEvent = { + target: { + name: "isShared", + type: "checkbox", + checked: e.checked, + }, + }; + handleRoomChange(syntheticEvent); + }} + > + + + + + Shared room + + + + {/* Webhook Configuration Section */} + + Webhook URL + + + Optional: URL to receive notifications when transcripts + are ready - {isEditing && ( + {room.webhookUrl && ( <> - - - {webhookTestResult && ( -
+ + Used for HMAC signature verification (auto-generated + if left empty) + + + + {isEditing && ( + <> + - {webhookTestResult} -
- )} -
+ + {webhookTestResult && ( +
+ {webhookTestResult} +
+ )} + + + )} )} - - )} +
- - { - const syntheticEvent = { - target: { - name: "isShared", - type: "checkbox", - checked: e.checked, - }, - }; - handleRoomChange(syntheticEvent); - }} - > - - - - - Shared room - - + + { + setRoomInput({ + ...room, + icsUrl: + settings.ics_url !== undefined + ? settings.ics_url + : room.icsUrl, + icsEnabled: + settings.ics_enabled !== undefined + ? settings.ics_enabled + : room.icsEnabled, + icsFetchInterval: + settings.ics_fetch_interval !== undefined + ? settings.ics_fetch_interval + : room.icsFetchInterval, + }); + }} + isOwner={true} + /> + - { - setRoomInput({ - ...room, - icsUrl: - settings.ics_url !== undefined - ? settings.ics_url - : room.icsUrl, - icsEnabled: - settings.ics_enabled !== undefined - ? settings.ics_enabled - : room.icsEnabled, - icsFetchInterval: - settings.ics_fetch_interval !== undefined - ? settings.ics_fetch_interval - : room.icsFetchInterval, - }); - }} - isOwner={true} - /> + + + { + const syntheticEvent = { + target: { + name: "zulipAutoPost", + type: "checkbox", + checked: e.checked, + }, + }; + handleRoomChange(syntheticEvent); + }} + > + + + + + + Automatically post transcription to Zulip + + + + + + Zulip stream + + setRoomInput({ + ...room, + zulipStream: e.value[0], + zulipTopic: "", + }) + } + collection={streamCollection} + disabled={!room.zulipAutoPost} + > + + + + + + + + + + + + {streamOptions.map((option) => ( + + {option.label} + + + ))} + + + + + + + Zulip topic + + setRoomInput({ ...room, zulipTopic: e.value[0] }) + } + collection={topicCollection} + disabled={!room.zulipAutoPost} + > + + + + + + + + + + + + {topicOptions.map((option) => ( + + {option.label} + + + ))} + + + + + +