mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-25 06:35:18 +00:00
Compare commits
3 Commits
v0.45.0
...
release-pl
| Author | SHA1 | Date | |
|---|---|---|---|
| eac8865270 | |||
|
|
52888f692f | ||
|
|
aa7f4cdb39 |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [0.45.1](https://github.com/GreyhavenHQ/reflector/compare/v0.45.0...v0.45.1) (2026-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* past due meetings are now 8h for ics ([#958](https://github.com/GreyhavenHQ/reflector/issues/958)) ([52888f6](https://github.com/GreyhavenHQ/reflector/commit/52888f692fee2c5c06f62e51230b0ecfd54b8814))
|
||||
|
||||
## [0.45.0](https://github.com/GreyhavenHQ/reflector/compare/v0.44.0...v0.45.0) (2026-04-09)
|
||||
|
||||
|
||||
|
||||
33
docs/pnpm-lock.yaml
generated
33
docs/pnpm-lock.yaml
generated
@@ -2252,11 +2252,11 @@ packages:
|
||||
resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
brace-expansion@1.1.13:
|
||||
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
|
||||
brace-expansion@1.1.14:
|
||||
resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
|
||||
|
||||
brace-expansion@2.0.3:
|
||||
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
|
||||
brace-expansion@2.1.0:
|
||||
resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
@@ -3011,9 +3011,8 @@ packages:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.3.2:
|
||||
resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==}
|
||||
engines: {node: '>=20'}
|
||||
dompurify@3.4.0:
|
||||
resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==}
|
||||
|
||||
domutils@2.8.0:
|
||||
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
||||
@@ -3286,8 +3285,8 @@ packages:
|
||||
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
|
||||
hasBin: true
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
follow-redirects@1.16.0:
|
||||
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
@@ -9378,12 +9377,12 @@ snapshots:
|
||||
widest-line: 4.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
|
||||
brace-expansion@1.1.13:
|
||||
brace-expansion@1.1.14:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@2.0.3:
|
||||
brace-expansion@2.1.0:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
@@ -10232,7 +10231,7 @@ snapshots:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.3.2:
|
||||
dompurify@3.4.0:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
@@ -10535,7 +10534,7 @@ snapshots:
|
||||
|
||||
flat@5.0.2: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
follow-redirects@1.16.0: {}
|
||||
|
||||
foreach@2.0.6: {}
|
||||
|
||||
@@ -10892,7 +10891,7 @@ snapshots:
|
||||
http-proxy@1.18.1:
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.15.11
|
||||
follow-redirects: 1.16.0
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -11470,7 +11469,7 @@ snapshots:
|
||||
d3-sankey: 0.12.3
|
||||
dagre-d3-es: 7.0.13
|
||||
dayjs: 1.11.19
|
||||
dompurify: 3.3.2
|
||||
dompurify: 3.4.0
|
||||
katex: 0.16.33
|
||||
khroma: 2.1.0
|
||||
lodash-es: 4.17.23
|
||||
@@ -11824,11 +11823,11 @@ snapshots:
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.13
|
||||
brace-expansion: 1.1.14
|
||||
|
||||
minimatch@5.1.8:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.3
|
||||
brace-expansion: 2.1.0
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
|
||||
@@ -83,9 +83,6 @@ def _should_sync(room) -> bool:
|
||||
return time_since_sync.total_seconds() >= room.ics_fetch_interval
|
||||
|
||||
|
||||
MEETING_DEFAULT_DURATION = timedelta(hours=1)
|
||||
|
||||
|
||||
async def create_upcoming_meetings_for_event(event, create_window, room: Room):
|
||||
if event.start_time <= create_window:
|
||||
return
|
||||
@@ -102,7 +99,9 @@ async def create_upcoming_meetings_for_event(event, create_window, room: Room):
|
||||
)
|
||||
|
||||
try:
|
||||
end_date = event.end_time or (event.start_time + MEETING_DEFAULT_DURATION)
|
||||
# 8h rejoin window matches manual on-the-fly meetings; the scheduled
|
||||
# DTEND stays in calendar_events.end_time for reference.
|
||||
end_date = event.start_time + timedelta(hours=8)
|
||||
|
||||
client = create_platform_client(room.platform)
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ import pytest
|
||||
from icalendar import Calendar, Event
|
||||
|
||||
from reflector.db import get_database
|
||||
from reflector.db.calendar_events import calendar_events_controller
|
||||
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
|
||||
from reflector.db.meetings import meetings_controller
|
||||
from reflector.db.rooms import rooms, rooms_controller
|
||||
from reflector.services.ics_sync import ics_sync_service
|
||||
from reflector.video_platforms.models import MeetingData
|
||||
from reflector.worker.ics_sync import (
|
||||
_should_sync,
|
||||
create_upcoming_meetings_for_event,
|
||||
sync_room_ics,
|
||||
)
|
||||
|
||||
@@ -225,6 +228,68 @@ async def test_sync_respects_fetch_interval():
|
||||
assert mock_delay.call_args[0][0] == room2.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_upcoming_meeting_uses_8h_end_date():
|
||||
# ICS-pre-created meetings get an 8h rejoin window anchored to the
|
||||
# scheduled start, ignoring the calendar event's DTEND. Regression
|
||||
# guard for the "Meeting has ended" bug when participants run over a
|
||||
# short scheduled window.
|
||||
room = await rooms_controller.add(
|
||||
name="ics-8h-room",
|
||||
user_id="test-user",
|
||||
zulip_auto_post=False,
|
||||
zulip_stream="",
|
||||
zulip_topic="",
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic-2nd-participant",
|
||||
is_shared=False,
|
||||
ics_url="https://calendar.example.com/ics-8h.ics",
|
||||
ics_enabled=True,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
event_start = now + timedelta(minutes=1)
|
||||
event_end = event_start + timedelta(minutes=30)
|
||||
|
||||
event = await calendar_events_controller.upsert(
|
||||
CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="ics-8h-evt",
|
||||
title="Short meeting that runs over",
|
||||
start_time=event_start,
|
||||
end_time=event_end,
|
||||
)
|
||||
)
|
||||
|
||||
create_window = now - timedelta(minutes=6)
|
||||
|
||||
fake_client = MagicMock()
|
||||
fake_client.create_meeting = AsyncMock(
|
||||
return_value=MeetingData(
|
||||
meeting_id="ics-8h-meeting",
|
||||
room_name=room.name,
|
||||
room_url="https://daily.example/ics-8h",
|
||||
host_room_url="https://daily.example/ics-8h",
|
||||
platform=room.platform,
|
||||
extra_data={},
|
||||
)
|
||||
)
|
||||
fake_client.upload_logo = AsyncMock(return_value=True)
|
||||
|
||||
with patch(
|
||||
"reflector.worker.ics_sync.create_platform_client",
|
||||
return_value=fake_client,
|
||||
):
|
||||
await create_upcoming_meetings_for_event(event, create_window, room)
|
||||
|
||||
meeting = await meetings_controller.get_by_calendar_event(event.id, room)
|
||||
assert meeting is not None
|
||||
assert meeting.start_date == event_start
|
||||
assert meeting.end_date == event_start + timedelta(hours=8)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_handles_errors_gracefully():
|
||||
room = await rooms_controller.add(
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "../lib/apiHooks";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { formatDateTime, formatStartedAgo } from "../lib/timeUtils";
|
||||
import { formatJoinError } from "../lib/errorUtils";
|
||||
import MeetingMinimalHeader from "../components/MeetingMinimalHeader";
|
||||
import { NonEmptyString } from "../lib/utils";
|
||||
import { MeetingId, assertMeetingId } from "../lib/types";
|
||||
@@ -188,6 +189,19 @@ export default function MeetingSelection({
|
||||
flex="1"
|
||||
gap={{ base: 4, md: 6 }}
|
||||
>
|
||||
{joinMeetingMutation.isError && (
|
||||
<Box
|
||||
p={4}
|
||||
borderRadius="md"
|
||||
bg="red.50"
|
||||
borderLeft="4px solid"
|
||||
borderColor="red.400"
|
||||
>
|
||||
<Text color="red.700">
|
||||
{formatJoinError(joinMeetingMutation.error)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{/* Current Ongoing Meetings - BIG DISPLAY */}
|
||||
{currentMeetings.length > 0 ? (
|
||||
<VStack align="stretch" gap={6} mb={8}>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useRoomJoinMeeting,
|
||||
useMeetingStartRecording,
|
||||
} from "../../lib/apiHooks";
|
||||
import { formatJoinError } from "../../lib/errorUtils";
|
||||
import { omit } from "remeda";
|
||||
import {
|
||||
assertExists,
|
||||
@@ -428,7 +429,7 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
||||
if (joinMutation.isError) {
|
||||
return (
|
||||
<Center width="100vw" height="100vh">
|
||||
<Text color="red.500">Failed to join meeting. Please try again.</Text>
|
||||
<Text color="red.500">{formatJoinError(joinMutation.error)}</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import type { components } from "../../reflector-api";
|
||||
import { useAuth } from "../../lib/AuthProvider";
|
||||
import { useRoomJoinMeeting } from "../../lib/apiHooks";
|
||||
import { formatJoinError } from "../../lib/errorUtils";
|
||||
import { assertMeetingId } from "../../lib/types";
|
||||
import {
|
||||
ConsentDialogButton,
|
||||
@@ -66,7 +67,6 @@ export default function LiveKitRoom({ meeting, room }: LiveKitRoomProps) {
|
||||
|
||||
const joinMutation = useRoomJoinMeeting();
|
||||
const [joinedMeeting, setJoinedMeeting] = useState<Meeting | null>(null);
|
||||
const [connectionError, setConnectionError] = useState(false);
|
||||
const [userChoices, setUserChoices] = useState<LocalUserChoices | null>(null);
|
||||
|
||||
// ── Consent dialog (same hooks as Daily/Whereby) ──────────
|
||||
@@ -99,7 +99,7 @@ export default function LiveKitRoom({ meeting, room }: LiveKitRoomProps) {
|
||||
}
|
||||
return "";
|
||||
})();
|
||||
const isJoining = !!userChoices && !joinedMeeting && !connectionError;
|
||||
const isJoining = !!userChoices && !joinedMeeting && !joinMutation.isError;
|
||||
|
||||
// ── Join meeting via backend API after PreJoin submit ─────
|
||||
useEffect(() => {
|
||||
@@ -123,7 +123,6 @@ export default function LiveKitRoom({ meeting, room }: LiveKitRoomProps) {
|
||||
if (!cancelled) setJoinedMeeting(result);
|
||||
} catch (err) {
|
||||
console.error("Failed to join LiveKit meeting:", err);
|
||||
if (!cancelled) setConnectionError(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,10 +181,10 @@ export default function LiveKitRoom({ meeting, room }: LiveKitRoomProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (connectionError) {
|
||||
if (joinMutation.isError) {
|
||||
return (
|
||||
<Center h="100vh" bg="gray.50">
|
||||
<Text fontSize="lg">Failed to connect to meeting</Text>
|
||||
<Text fontSize="lg">{formatJoinError(joinMutation.error)}</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -863,16 +863,9 @@ export function useRoomGetMeeting(
|
||||
}
|
||||
|
||||
export function useRoomJoinMeeting() {
|
||||
const { setError } = useError();
|
||||
|
||||
return $api.useMutation(
|
||||
"post",
|
||||
"/v1/rooms/{room_name}/meetings/{meeting_id}/join",
|
||||
{
|
||||
onError: (error) => {
|
||||
setError(error as Error, "There was an error joining the meeting");
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
import { isNonEmptyArray, NonEmptyArray } from "./array";
|
||||
|
||||
export function getErrorDetail(error: unknown, fallback: string): string {
|
||||
if (!error) return fallback;
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const detail = (error as { detail?: unknown }).detail;
|
||||
if (typeof detail === "string" && detail.length > 0) return detail;
|
||||
const response = (error as { response?: { data?: { detail?: unknown } } })
|
||||
.response;
|
||||
const nestedDetail = response?.data?.detail;
|
||||
if (typeof nestedDetail === "string" && nestedDetail.length > 0)
|
||||
return nestedDetail;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function formatJoinError(error: unknown): string {
|
||||
const detail = getErrorDetail(error, "");
|
||||
switch (detail) {
|
||||
case "Meeting has ended":
|
||||
return "This meeting has ended. The organizer can start a new one.";
|
||||
case "Meeting is not active":
|
||||
return "This meeting is no longer active. Ask the organizer to start it again.";
|
||||
case "Meeting not found":
|
||||
return "This meeting no longer exists. Check the link or ask the organizer for a new one.";
|
||||
case "Room not found":
|
||||
return "This room doesn't exist.";
|
||||
default:
|
||||
return detail || "We couldn't join the meeting. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldShowError(error: Error | null | undefined) {
|
||||
if (
|
||||
error?.name == "ResponseError" &&
|
||||
|
||||
Reference in New Issue
Block a user