diff --git a/client/src/components/Schedule.tsx b/client/src/components/Schedule.tsx
index f0427f2..9dc861f 100644
--- a/client/src/components/Schedule.tsx
+++ b/client/src/components/Schedule.tsx
@@ -96,6 +96,9 @@ function Schedule({ selectedDay, slots, events }: Props) {
function handleClickCancel() {
setDraftMode(false);
+ setStartTime("");
+ setEndTime("");
+ setDuration(undefined);
}
async function addSchesuledSession() {
@@ -129,6 +132,9 @@ function Schedule({ selectedDay, slots, events }: Props) {
function handleClickScheduleDemo() {
setDateForInstantStart(undefined);
setStartAt(new Date());
+ setStartTime("");
+ setEndTime("");
+ setDuration(undefined);
setDraftMode(true);
}
@@ -184,9 +190,12 @@ function Schedule({ selectedDay, slots, events }: Props) {
useEffect(() => {
function handleEscKey(e: KeyboardEvent) {
- if (!draftMode && e.key !== "Escape") return;
+ if (!draftMode || e.key !== "Escape") return;
setDraftMode(false);
+ setStartTime("");
+ setEndTime("");
+ setDuration(undefined);
}
document.addEventListener("keyup", handleEscKey, false);
@@ -194,7 +203,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
return () => {
document.removeEventListener("keyup", handleEscKey, false);
};
- }, []);
+ }, [draftMode]);
return (
diff --git a/server/src/routes/scheduledSessions.ts b/server/src/routes/scheduledSessions.ts
index 6bec1b6..6916be1 100644
--- a/server/src/routes/scheduledSessions.ts
+++ b/server/src/routes/scheduledSessions.ts
@@ -4,6 +4,7 @@ import Build from "../models/Build.js";
import Schedule from "../models/Schedule.js";
import {
addMinutes,
+ addSeconds,
areIntervalsOverlapping,
endOfDay,
isValid,
@@ -78,7 +79,7 @@ router.get("/:buildId", async (req, res) => {
router.post("/", async (req, res) => {
const { companyId, buildId, slot, startAt, client, duration } = req.body;
- const startAtISO = parseISO(startAt);
+ let startAtISO = parseISO(startAt);
if (!isValid(startAtISO)) {
return res.status(400).json({
@@ -96,9 +97,50 @@ router.post("/", async (req, res) => {
});
}
+ // Check for conflicts at the same minute and adjust time if needed
+ // This prevents multiple sessions from starting simultaneously on the same server
+ // We check all sessions regardless of company, build, or slot to prevent server conflicts
+ let adjustedStartAt = startAtISO;
+ let maxAttempts = 100; // Prevent infinite loop
+ let attempts = 0;
+
+ while (attempts < maxAttempts) {
+ // Find sessions starting within 10 seconds of our target time
+ // This ensures sessions don't start simultaneously on the same session server
+ const conflictingSessions = await ScheduledSession.find({
+ startAt: {
+ $gte: adjustedStartAt,
+ $lt: addSeconds(adjustedStartAt, 10),
+ },
+ });
+
+ if (conflictingSessions.length === 0) {
+ // No conflict found, use this time
+ break;
+ }
+
+ // Find the latest conflicting session and schedule after it
+ // This gives the first build time to start and fill video memory
+ // so the second build will start on another server with more available memory
+ const latestSession = conflictingSessions.reduce((latest, session) =>
+ new Date(session.startAt) > new Date(latest.startAt) ? session : latest
+ );
+
+ adjustedStartAt = addSeconds(new Date(latestSession.startAt), 10);
+ attempts++;
+ }
+
+ if (attempts >= maxAttempts) {
+ return res.status(400).json({
+ status: "error",
+ message: "Не удалось найти свободное время для планирования сеанса",
+ });
+ }
+
+ startAtISO = adjustedStartAt;
const endAtISO = addMinutes(startAtISO, duration);
- // Check for overlapping sessions first
+ // Check for overlapping sessions in the same slot
const scheduledSessions = await ScheduledSession.find({
companyId,
buildId,