From 1d5a22ad1d71304163fb785164e84f7c8fe640e0 Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Tue, 2 Sep 2025 18:52:31 -0400 Subject: [PATCH] room detail page fix --- server/runserver.sh | 2 +- www/app/(app)/rooms/page.tsx | 122 ++++++++++----------- www/app/(app)/rooms/useRoomList.tsx | 2 +- www/app/lib/apiHooks.ts | 28 +++++ www/app/reflector-api.d.ts | 163 ++++++++++++++++++++++++++-- 5 files changed, 240 insertions(+), 77 deletions(-) diff --git a/server/runserver.sh b/server/runserver.sh index a4fb6869..9cccaacb 100755 --- a/server/runserver.sh +++ b/server/runserver.sh @@ -2,7 +2,7 @@ if [ "${ENTRYPOINT}" = "server" ]; then uv run alembic upgrade head - uv run -m reflector.app + uv run uvicorn reflector.app:app --host 0.0.0.0 --port 1250 elif [ "${ENTRYPOINT}" = "worker" ]; then uv run celery -A reflector.worker.app worker --loglevel=info elif [ "${ENTRYPOINT}" = "beat" ]; then diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index 99644297..bf96d367 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -15,11 +15,9 @@ import { createListCollection, useDisclosure, } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { LuEye, LuEyeOff } from "react-icons/lu"; -import useApi from "../../lib/useApi"; import useRoomList from "./useRoomList"; -import { ApiError, RoomDetails } from "../../api"; import type { components } from "../../reflector-api"; import { useRoomCreate, @@ -27,9 +25,12 @@ import { useRoomDelete, useZulipStreams, useZulipTopics, + useRoomGet, + useRoomTestWebhook, } from "../../lib/apiHooks"; import { RoomList } from "./_components/RoomList"; import { PaginationPage } from "../browse/_components/Pagination"; +import { assertExists } from "../../lib/utils"; type Room = components["schemas"]["Room"]; @@ -86,12 +87,10 @@ export default function RoomsList() { const recordingTypeCollection = createListCollection({ items: recordingTypeOptions, }); - const [room, setRoom] = useState(roomInitialState); + const [room_, setRoom] = useState(roomInitialState); const [isEditing, setIsEditing] = useState(false); - const [editRoomId, setEditRoomId] = useState(""); - // TODO seems to be no setPage calls - const [page, setPage] = useState(1); - const { loading, response, refetch } = useRoomList(PaginationPage(page)); + const [editRoomId, setEditRoomId] = useState(null); + const { loading, response, refetch } = useRoomList(PaginationPage(1)); const [nameError, setNameError] = useState(""); const [linkCopied, setLinkCopied] = useState(""); const [selectedStreamId, setSelectedStreamId] = useState(null); @@ -100,14 +99,6 @@ export default function RoomsList() { null, ); const [showWebhookSecret, setShowWebhookSecret] = useState(false); - interface Stream { - stream_id: number; - name: string; - } - - interface Topic { - name: string; - } const createRoomMutation = useRoomCreate(); const updateRoomMutation = useRoomUpdate(); @@ -115,6 +106,38 @@ export default function RoomsList() { const { data: streams = [] } = useZulipStreams(); const { data: topics = [] } = useZulipTopics(selectedStreamId); + const { + data: detailedEditedRoom, + isLoading: isDetailedEditedRoomLoading, + error: detailedEditedRoomError, + } = useRoomGet(editRoomId); + + // room being edited, as fetched from the server + const editedRoom: typeof roomInitialState | null = useMemo( + () => + detailedEditedRoom + ? { + name: detailedEditedRoom.name, + zulipAutoPost: detailedEditedRoom.zulip_auto_post, + zulipStream: detailedEditedRoom.zulip_stream, + zulipTopic: detailedEditedRoom.zulip_topic, + isLocked: detailedEditedRoom.is_locked, + roomMode: detailedEditedRoom.room_mode, + recordingType: detailedEditedRoom.recording_type, + recordingTrigger: detailedEditedRoom.recording_trigger, + isShared: detailedEditedRoom.is_shared, + webhookUrl: detailedEditedRoom.webhook_url || "", + webhookSecret: detailedEditedRoom.webhook_secret || "", + } + : null, + [detailedEditedRoom], + ); + + // here for minimal change in unrelated PR to make it work "backward-compatible" way. TODO make sense of it + const room = editedRoom || room_; + + const roomTestWebhookMutation = useRoomTestWebhook(); + // Update selected stream ID when zulip stream changes useEffect(() => { if (room.zulipStream && streams.length > 0) { @@ -161,31 +184,37 @@ export default function RoomsList() { }; const handleTestWebhook = async () => { - if (!room.webhookUrl || !editRoomId) { + if (!room.webhookUrl) { setWebhookTestResult("Please enter a webhook URL first"); return; } + if (!editRoomId) { + console.error("No room ID to test webhook"); + return; + } setTestingWebhook(true); setWebhookTestResult(null); try { - const response = await api?.v1RoomsTestWebhook({ - roomId: editRoomId, + const response = await roomTestWebhookMutation.mutateAsync({ + params: { + path: { + room_id: editRoomId, + }, + }, }); - if (response?.success) { + if (response.success) { setWebhookTestResult( `✅ Webhook test successful! Status: ${response.status_code}`, ); } else { let errorMsg = `❌ Webhook test failed`; - if (response?.status_code) { - errorMsg += ` (Status: ${response.status_code})`; - } - if (response?.error) { + errorMsg += ` (Status: ${response.status_code})`; + if (response.error) { errorMsg += `: ${response.error}`; - } else if (response?.response_preview) { + } else if (response.response_preview) { // Try to parse and extract meaningful error from response // Specific to N8N at the moment, as there is no specification for that // We could just display as is, but decided here to dig a little bit more. @@ -241,7 +270,7 @@ export default function RoomsList() { if (isEditing) { await updateRoomMutation.mutateAsync({ params: { - path: { room_id: editRoomId }, + path: { room_id: assertExists(editRoomId) }, }, body: roomData, }); @@ -272,46 +301,11 @@ export default function RoomsList() { } }; - const handleEditRoom = async (roomId, roomData) => { + const handleEditRoom = async (roomId: string, roomData) => { // Reset states setShowWebhookSecret(false); setWebhookTestResult(null); - // Fetch full room details to get webhook fields - try { - const detailedRoom = await api?.v1RoomsGet({ roomId }); - if (detailedRoom) { - setRoom({ - name: detailedRoom.name, - zulipAutoPost: detailedRoom.zulip_auto_post, - zulipStream: detailedRoom.zulip_stream, - zulipTopic: detailedRoom.zulip_topic, - isLocked: detailedRoom.is_locked, - roomMode: detailedRoom.room_mode, - recordingType: detailedRoom.recording_type, - recordingTrigger: detailedRoom.recording_trigger, - isShared: detailedRoom.is_shared, - webhookUrl: detailedRoom.webhook_url || "", - webhookSecret: detailedRoom.webhook_secret || "", - }); - } - } catch (error) { - console.error("Failed to fetch room details, using list data:", error); - // Fallback to using the data from the list - setRoom({ - name: roomData.name, - zulipAutoPost: roomData.zulip_auto_post, - zulipStream: roomData.zulip_stream, - zulipTopic: roomData.zulip_topic, - isLocked: roomData.is_locked, - roomMode: roomData.room_mode, - recordingType: roomData.recording_type, - recordingTrigger: roomData.recording_trigger, - isShared: roomData.is_shared, - webhookUrl: roomData.webhook_url || "", - webhookSecret: roomData.webhook_secret || "", - }); - } setEditRoomId(roomId); setIsEditing(true); setNameError(""); @@ -346,9 +340,9 @@ export default function RoomsList() { }); }; - const myRooms: RoomDetails[] = + const myRooms: Room[] = response?.items.filter((roomData) => !roomData.is_shared) || []; - const sharedRooms: RoomDetails[] = + const sharedRooms: Room[] = response?.items.filter((roomData) => roomData.is_shared) || []; if (loading && !response) diff --git a/www/app/(app)/rooms/useRoomList.tsx b/www/app/(app)/rooms/useRoomList.tsx index 4cf364c1..4daac97e 100644 --- a/www/app/(app)/rooms/useRoomList.tsx +++ b/www/app/(app)/rooms/useRoomList.tsx @@ -1,7 +1,7 @@ import { useRoomsList } from "../../lib/apiHooks"; import type { components } from "../../reflector-api"; -type Page_Room_ = components["schemas"]["Page_Room_"]; +type Page_Room_ = components["schemas"]["Page_RoomDetails_"]; import { PaginationPage } from "../browse/_components/Pagination"; type RoomList = { diff --git a/www/app/lib/apiHooks.ts b/www/app/lib/apiHooks.ts index 58f600ce..8dc738f4 100644 --- a/www/app/lib/apiHooks.ts +++ b/www/app/lib/apiHooks.ts @@ -120,6 +120,34 @@ export function useTranscriptGet(transcriptId: string | null) { ); } +export function useRoomGet(roomId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/rooms/{room_id}", + { + params: { + path: { room_id: roomId || "" }, + }, + }, + { + enabled: !!roomId && isAuthenticated, + staleTime: STALE_TIME, + }, + ); +} + +export function useRoomTestWebhook() { + const { setError } = useError(); + + return $api.useMutation("post", "/v1/rooms/{room_id}/webhook/test", { + onError: (error) => { + setError(error as Error, "There was an error testing the webhook"); + }, + }); +} + export function useRoomCreate() { const { setError } = useError(); const queryClient = useQueryClient(); diff --git a/www/app/reflector-api.d.ts b/www/app/reflector-api.d.ts index 711613be..7f9f073e 100644 --- a/www/app/reflector-api.d.ts +++ b/www/app/reflector-api.d.ts @@ -66,7 +66,8 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** Rooms Get */ + get: operations["v1_rooms_get"]; put?: never; post?: never; /** Rooms Delete */ @@ -94,6 +95,26 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/rooms/{room_id}/webhook/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Rooms Test Webhook + * @description Test webhook configuration by sending a sample payload. + */ + post: operations["v1_rooms_test_webhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/transcripts": { parameters: { query?: never; @@ -511,6 +532,10 @@ export interface components { recording_trigger: string; /** Is Shared */ is_shared: boolean; + /** Webhook Url */ + webhook_url: string; + /** Webhook Secret */ + webhook_secret: string; }; /** CreateTranscript */ CreateTranscript: { @@ -749,10 +774,10 @@ export interface components { /** Pages */ pages?: number | null; }; - /** Page[Room] */ - Page_Room_: { + /** Page[RoomDetails] */ + Page_RoomDetails_: { /** Items */ - items: components["schemas"]["Room"][]; + items: components["schemas"]["RoomDetails"][]; /** Total */ total?: number | null; /** Page */ @@ -801,6 +826,40 @@ export interface components { /** Is Shared */ is_shared: boolean; }; + /** RoomDetails */ + RoomDetails: { + /** Id */ + id: string; + /** Name */ + name: string; + /** User Id */ + user_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + /** Webhook Url */ + webhook_url: string | null; + /** Webhook Secret */ + webhook_secret: string | null; + }; /** RtcOffer */ RtcOffer: { /** Sdp */ @@ -817,11 +876,8 @@ export interface components { * @description Total number of search results */ total: number; - /** - * Query - * @description Search query text - */ - query: string; + /** Query */ + query?: string | null; /** * Limit * @description Results per page @@ -955,6 +1011,10 @@ export interface components { recording_trigger: string; /** Is Shared */ is_shared: boolean; + /** Webhook Url */ + webhook_url: string; + /** Webhook Secret */ + webhook_secret: string; }; /** UpdateTranscript */ UpdateTranscript: { @@ -995,6 +1055,25 @@ export interface components { /** Error Type */ type: string; }; + /** WebhookTestResult */ + WebhookTestResult: { + /** Success */ + success: boolean; + /** + * Message + * @default + */ + message: string; + /** + * Error + * @default + */ + error: string; + /** Status Code */ + status_code?: number | null; + /** Response Preview */ + response_preview?: string | null; + }; /** WherebyWebhookEvent */ WherebyWebhookEvent: { /** Apiversion */ @@ -1117,7 +1196,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Page_Room_"]; + "application/json": components["schemas"]["Page_RoomDetails_"]; }; }; /** @description Validation Error */ @@ -1164,6 +1243,37 @@ export interface operations { }; }; }; + v1_rooms_get: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoomDetails"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; v1_rooms_delete: { parameters: { query?: never; @@ -1216,7 +1326,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Room"]; + "application/json": components["schemas"]["RoomDetails"]; }; }; /** @description Validation Error */ @@ -1261,6 +1371,37 @@ export interface operations { }; }; }; + v1_rooms_test_webhook: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookTestResult"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; v1_transcripts_list: { parameters: { query?: {