From e6315c7ec706914694373c5a984c5c5b167a8422 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Tue, 7 Oct 2025 11:56:49 -0500 Subject: [PATCH 1/5] fix: preserve intentional gaps when reordering Run of Show items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, moving items in the Run of Show would recalculate ALL start times based on cumulative duration, destroying carefully planned gaps between items (stage changes, intermissions, etc.). Now implements smart gap-preserving logic: - Items with positive gaps (intentional buffers) → gap size preserved - Items with no gaps or overlaps → auto-adjusted to follow previous item - Items without start times → calculated automatically - Item numbers stay sequential after reordering This allows users to maintain their production timing intentions while reordering the show flow. Fixes issue where reordering would mess up all timings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/pages/RunOfShowEditor.tsx | 44 +++++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/web/src/pages/RunOfShowEditor.tsx b/apps/web/src/pages/RunOfShowEditor.tsx index e7c53c1..796df32 100644 --- a/apps/web/src/pages/RunOfShowEditor.tsx +++ b/apps/web/src/pages/RunOfShowEditor.tsx @@ -522,9 +522,9 @@ const RunOfShowEditor: React.FC = () => { // Swap items [items[currentIndex], items[targetIndex]] = [items[targetIndex], items[currentIndex]]; - // Recalculate item numbers and start times + // Recalculate item numbers and start times with gap preservation let itemCount = 1; - let cumulativeTimeSeconds = 0; + let cumulativeEndTimeSeconds = 0; const recalculatedItems = items.map((item) => { if (item.type === "item") { @@ -532,17 +532,45 @@ const RunOfShowEditor: React.FC = () => { item.itemNumber = itemCount.toString(); itemCount++; - // Update start time based on cumulative time - item.startTime = formatSecondsToTime(cumulativeTimeSeconds); + // Gap-preserving start time logic + const existingStartTime = parseTimeToSeconds(item.startTime || ""); + const expectedStartTime = cumulativeEndTimeSeconds; - // Add this item's duration to cumulative time + if (existingStartTime !== null) { + // Item HAS a start time - calculate gap + const gap = existingStartTime - expectedStartTime; + + if (gap > 0) { + // Positive gap (intentional buffer/break) - preserve it + item.startTime = formatSecondsToTime(expectedStartTime + gap); + cumulativeEndTimeSeconds = expectedStartTime + gap; + } else { + // No gap or overlap - fix it by setting to expected time + item.startTime = formatSecondsToTime(expectedStartTime); + cumulativeEndTimeSeconds = expectedStartTime; + } + } else { + // Item has NO start time - calculate it from expected time + item.startTime = formatSecondsToTime(expectedStartTime); + cumulativeEndTimeSeconds = expectedStartTime; + } + + // Add this item's duration to cumulative end time const durationSeconds = parseDurationToSeconds(item.duration || ""); if (durationSeconds !== null) { - cumulativeTimeSeconds += durationSeconds; + cumulativeEndTimeSeconds += durationSeconds; } } else if (item.type === "header") { - // Headers can have a start time matching the next item - item.startTime = formatSecondsToTime(cumulativeTimeSeconds); + // Headers use current cumulative time but don't affect it + const existingStartTime = parseTimeToSeconds(item.startTime || ""); + + if (existingStartTime !== null) { + // Header HAS a start time - preserve it as-is + // Don't update cumulative time + } else { + // Header has NO start time - set it to current cumulative time + item.startTime = formatSecondsToTime(cumulativeEndTimeSeconds); + } } return item; }); From 256df11b010c206d14c4d507a6a46b936d2d99a2 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Tue, 7 Oct 2025 12:02:35 -0500 Subject: [PATCH 2/5] fix: correct gap calculation logic for Run of Show reordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation had a critical flaw: it compared an item's old absolute start time with the end time of its new preceding item, resulting in incorrect gap calculations. This commit fixes the logic by: 1. Pre-calculating gaps BEFORE reordering - Each item's gap is calculated relative to its CURRENT previous item - Only positive gaps (intentional buffers) are preserved 2. Storing gap values with each item temporarily 3. After reordering, using the stored gaps to set new start times - New start time = previous item's end time + stored gap - This preserves the gap size regardless of position 4. Fixing header timing inconsistencies - Headers with times earlier than cumulative time are adjusted - Prevents misleading header timestamps after reordering Example fix: Before: Item with 5-min gap incorrectly calculated as 13-min gap After: Item correctly maintains its 5-min gap after reordering Addresses PR code review suggestions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/pages/RunOfShowEditor.tsx | 88 ++++++++++++++++++-------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/apps/web/src/pages/RunOfShowEditor.tsx b/apps/web/src/pages/RunOfShowEditor.tsx index 796df32..b8819b3 100644 --- a/apps/web/src/pages/RunOfShowEditor.tsx +++ b/apps/web/src/pages/RunOfShowEditor.tsx @@ -519,41 +519,66 @@ const RunOfShowEditor: React.FC = () => { if (targetIndex >= items.length) return; } - // Swap items - [items[currentIndex], items[targetIndex]] = [items[targetIndex], items[currentIndex]]; + // Step 1: Calculate gaps BEFORE reordering + // This captures the gap each item has relative to its CURRENT previous item + const itemsWithGaps = items.map((item, index) => { + let gap = 0; - // Recalculate item numbers and start times with gap preservation + if (item.type === "item") { + const itemStartTime = parseTimeToSeconds(item.startTime || ""); + + if (itemStartTime !== null && index > 0) { + // Calculate cumulative end time of all previous items + let prevEndTime = 0; + for (let i = 0; i < index; i++) { + const prevItem = items[i]; + if (prevItem.type === "item") { + const prevStart = parseTimeToSeconds(prevItem.startTime || ""); + const prevDuration = parseDurationToSeconds(prevItem.duration || ""); + + if (prevStart !== null) { + prevEndTime = prevStart; + if (prevDuration !== null) { + prevEndTime += prevDuration; + } + } else if (prevDuration !== null) { + prevEndTime += prevDuration; + } + } + } + + // Gap is the difference between this item's start and when it "should" start + gap = itemStartTime - prevEndTime; + // Only preserve positive gaps (intentional buffers) + if (gap < 0) gap = 0; + } + } + + return { ...item, calculatedGap: gap }; + }); + + // Step 2: Swap items + [itemsWithGaps[currentIndex], itemsWithGaps[targetIndex]] = [ + itemsWithGaps[targetIndex], + itemsWithGaps[currentIndex], + ]; + + // Step 3: Recalculate item numbers and start times using stored gaps let itemCount = 1; let cumulativeEndTimeSeconds = 0; - const recalculatedItems = items.map((item) => { + const recalculatedItems = itemsWithGaps.map((item) => { if (item.type === "item") { // Update item number item.itemNumber = itemCount.toString(); itemCount++; - // Gap-preserving start time logic - const existingStartTime = parseTimeToSeconds(item.startTime || ""); - const expectedStartTime = cumulativeEndTimeSeconds; + // Use the pre-calculated gap from before the move + const gap = item.calculatedGap || 0; + const newStartTime = cumulativeEndTimeSeconds + gap; - if (existingStartTime !== null) { - // Item HAS a start time - calculate gap - const gap = existingStartTime - expectedStartTime; - - if (gap > 0) { - // Positive gap (intentional buffer/break) - preserve it - item.startTime = formatSecondsToTime(expectedStartTime + gap); - cumulativeEndTimeSeconds = expectedStartTime + gap; - } else { - // No gap or overlap - fix it by setting to expected time - item.startTime = formatSecondsToTime(expectedStartTime); - cumulativeEndTimeSeconds = expectedStartTime; - } - } else { - // Item has NO start time - calculate it from expected time - item.startTime = formatSecondsToTime(expectedStartTime); - cumulativeEndTimeSeconds = expectedStartTime; - } + item.startTime = formatSecondsToTime(newStartTime); + cumulativeEndTimeSeconds = newStartTime; // Add this item's duration to cumulative end time const durationSeconds = parseDurationToSeconds(item.duration || ""); @@ -565,14 +590,21 @@ const RunOfShowEditor: React.FC = () => { const existingStartTime = parseTimeToSeconds(item.startTime || ""); if (existingStartTime !== null) { - // Header HAS a start time - preserve it as-is - // Don't update cumulative time + // Header HAS a start time - ensure it's not before the cumulative time + if (existingStartTime < cumulativeEndTimeSeconds) { + item.startTime = formatSecondsToTime(cumulativeEndTimeSeconds); + } + // Otherwise, preserve its original time, as it's either on time or creates a gap. } else { // Header has NO start time - set it to current cumulative time item.startTime = formatSecondsToTime(cumulativeEndTimeSeconds); } } - return item; + + // Remove the temporary calculatedGap property + const { calculatedGap, ...itemWithoutGap } = item; + void calculatedGap; // Mark as intentionally unused + return itemWithoutGap as RunOfShowItem; }); setRunOfShow({ ...runOfShow, items: recalculatedItems }); From 8cf7c30c78340176624acb8626b27ae53e940719 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Tue, 7 Oct 2025 12:08:31 -0500 Subject: [PATCH 3/5] refactor: fix gap calculation and improve Run of Show reordering logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses three critical issues in the reordering logic: 1. Fixed gap calculation to use immediate previous item - Previous: Incorrectly accumulated ALL previous items' times - Now: Walks backward to find the immediate previous item, skipping headers - Example fix: Item with 2-min gap was incorrectly calculated as 7-min gap 2. Preserve absolute schedule offset - Previous: Always started recalculation from 00:00:00 - Now: Finds first item with a time and uses that as the base - Benefit: Shows starting at 20:00 (8 PM) maintain that offset after reordering 3. Implement immutable state updates - Previous: Mutated items directly in .map() function - Now: Creates new objects with updated properties - Benefit: Follows React best practices and prevents potential state bugs Additional improvements: - Early return for items without start times during gap calculation - Better header time handling (advance cumulative time if header is ahead) - Cleaner separation of header vs item logic in recalculation These changes ensure gaps are correctly preserved, absolute schedule times are maintained, and React state management follows best practices. Addresses PR code review suggestions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/pages/RunOfShowEditor.tsx | 118 ++++++++++++++----------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/apps/web/src/pages/RunOfShowEditor.tsx b/apps/web/src/pages/RunOfShowEditor.tsx index b8819b3..64ae36f 100644 --- a/apps/web/src/pages/RunOfShowEditor.tsx +++ b/apps/web/src/pages/RunOfShowEditor.tsx @@ -520,37 +520,43 @@ const RunOfShowEditor: React.FC = () => { } // Step 1: Calculate gaps BEFORE reordering - // This captures the gap each item has relative to its CURRENT previous item + // This captures the gap each item has relative to its IMMEDIATE previous item const itemsWithGaps = items.map((item, index) => { let gap = 0; if (item.type === "item") { const itemStartTime = parseTimeToSeconds(item.startTime || ""); - if (itemStartTime !== null && index > 0) { - // Calculate cumulative end time of all previous items - let prevEndTime = 0; - for (let i = 0; i < index; i++) { - const prevItem = items[i]; - if (prevItem.type === "item") { - const prevStart = parseTimeToSeconds(prevItem.startTime || ""); - const prevDuration = parseDurationToSeconds(prevItem.duration || ""); - - if (prevStart !== null) { - prevEndTime = prevStart; - if (prevDuration !== null) { - prevEndTime += prevDuration; - } - } else if (prevDuration !== null) { - prevEndTime += prevDuration; - } - } + if (itemStartTime === null) { + // Without a valid start time we cannot compute a reliable gap + return { ...item, calculatedGap: 0 }; + } + + // Walk backwards to find the previous item's end time, skipping headers + let prevEndTime: number | null = null; + for (let i = index - 1; i >= 0; i--) { + const prev = items[i]; + if (prev.type !== "item") continue; + + const prevStart = parseTimeToSeconds(prev.startTime || ""); + const prevDuration = parseDurationToSeconds(prev.duration || ""); + + if (prevStart !== null) { + prevEndTime = prevStart + (prevDuration ?? 0); + break; } - // Gap is the difference between this item's start and when it "should" start + // If no start, but we have duration, keep looking further back + // However, don't add duration without a known anchor start. + if (prevStart === null) continue; + } + + if (prevEndTime !== null) { gap = itemStartTime - prevEndTime; - // Only preserve positive gaps (intentional buffers) if (gap < 0) gap = 0; + } else { + // No previous item with a valid start; treat as no gap to preserve + gap = 0; } } @@ -563,47 +569,59 @@ const RunOfShowEditor: React.FC = () => { itemsWithGaps[currentIndex], ]; + // Establish initial cumulative time from earliest anchored start (header or item) + const firstAnchoredStart = (() => { + for (const it of itemsWithGaps) { + const t = parseTimeToSeconds(it.startTime || ""); + if (t !== null) return t; + } + return 0; + })(); + // Step 3: Recalculate item numbers and start times using stored gaps let itemCount = 1; - let cumulativeEndTimeSeconds = 0; + let cumulativeEndTimeSeconds = firstAnchoredStart; const recalculatedItems = itemsWithGaps.map((item) => { - if (item.type === "item") { - // Update item number - item.itemNumber = itemCount.toString(); - itemCount++; - - // Use the pre-calculated gap from before the move - const gap = item.calculatedGap || 0; - const newStartTime = cumulativeEndTimeSeconds + gap; - - item.startTime = formatSecondsToTime(newStartTime); - cumulativeEndTimeSeconds = newStartTime; - - // Add this item's duration to cumulative end time - const durationSeconds = parseDurationToSeconds(item.duration || ""); - if (durationSeconds !== null) { - cumulativeEndTimeSeconds += durationSeconds; - } - } else if (item.type === "header") { - // Headers use current cumulative time but don't affect it + if (item.type === "header") { const existingStartTime = parseTimeToSeconds(item.startTime || ""); + let newHeaderStart: string; if (existingStartTime !== null) { - // Header HAS a start time - ensure it's not before the cumulative time - if (existingStartTime < cumulativeEndTimeSeconds) { - item.startTime = formatSecondsToTime(cumulativeEndTimeSeconds); + // Advance pointer to header time if it's ahead, clamp if behind + if (existingStartTime > cumulativeEndTimeSeconds) { + cumulativeEndTimeSeconds = existingStartTime; + newHeaderStart = item.startTime!; + } else { + newHeaderStart = formatSecondsToTime(cumulativeEndTimeSeconds); } - // Otherwise, preserve its original time, as it's either on time or creates a gap. } else { - // Header has NO start time - set it to current cumulative time - item.startTime = formatSecondsToTime(cumulativeEndTimeSeconds); + newHeaderStart = formatSecondsToTime(cumulativeEndTimeSeconds); } + + const updatedHeader = { ...item, startTime: newHeaderStart } as RunOfShowItem; + const { calculatedGap, ...itemWithoutGap } = updatedHeader as any; + return itemWithoutGap as RunOfShowItem; + } + + // Items + const gap = (item as any).calculatedGap || 0; + const newStartTime = cumulativeEndTimeSeconds + gap; + const durationSeconds = parseDurationToSeconds(item.duration || ""); + + const updatedItem = { + ...item, + itemNumber: itemCount.toString(), + startTime: formatSecondsToTime(newStartTime), + } as RunOfShowItem; + + itemCount++; + cumulativeEndTimeSeconds = newStartTime; + if (durationSeconds !== null) { + cumulativeEndTimeSeconds += durationSeconds; } - // Remove the temporary calculatedGap property - const { calculatedGap, ...itemWithoutGap } = item; - void calculatedGap; // Mark as intentionally unused + const { calculatedGap, ...itemWithoutGap } = updatedItem as any; return itemWithoutGap as RunOfShowItem; }); From c44bafa26e8b3b4a833c8dfbefc9ce2b6aa05db0 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Tue, 7 Oct 2025 12:11:22 -0500 Subject: [PATCH 4/5] fix: resolve ESLint errors in Run of Show reordering logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed TypeScript ESLint violations: - Removed @typescript-eslint/no-explicit-any errors by properly typing objects - Removed @typescript-eslint/no-unused-vars errors for destructured calculatedGap Changes: - Instead of destructuring to remove calculatedGap, explicitly construct clean objects with only the needed properties - Properly typed itemWithGap as RunOfShowItem & { calculatedGap?: number } - Added logic to preserve custom column values while excluding calculatedGap This maintains the same functionality while satisfying strict TypeScript rules. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/pages/RunOfShowEditor.tsx | 68 +++++++++++++++++++++----- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/apps/web/src/pages/RunOfShowEditor.tsx b/apps/web/src/pages/RunOfShowEditor.tsx index 64ae36f..927caf4 100644 --- a/apps/web/src/pages/RunOfShowEditor.tsx +++ b/apps/web/src/pages/RunOfShowEditor.tsx @@ -599,30 +599,72 @@ const RunOfShowEditor: React.FC = () => { newHeaderStart = formatSecondsToTime(cumulativeEndTimeSeconds); } - const updatedHeader = { ...item, startTime: newHeaderStart } as RunOfShowItem; - const { calculatedGap, ...itemWithoutGap } = updatedHeader as any; - return itemWithoutGap as RunOfShowItem; + // Return header without calculatedGap property + return { + id: item.id, + type: item.type, + itemNumber: item.itemNumber, + startTime: newHeaderStart, + highlightColor: item.highlightColor, + headerTitle: item.headerTitle, + } as RunOfShowItem; } - // Items - const gap = (item as any).calculatedGap || 0; + // Items - extract gap and other properties + const itemWithGap = item as RunOfShowItem & { calculatedGap?: number }; + const gap = itemWithGap.calculatedGap || 0; const newStartTime = cumulativeEndTimeSeconds + gap; const durationSeconds = parseDurationToSeconds(item.duration || ""); - const updatedItem = { - ...item, - itemNumber: itemCount.toString(), - startTime: formatSecondsToTime(newStartTime), - } as RunOfShowItem; - itemCount++; cumulativeEndTimeSeconds = newStartTime; if (durationSeconds !== null) { cumulativeEndTimeSeconds += durationSeconds; } - const { calculatedGap, ...itemWithoutGap } = updatedItem as any; - return itemWithoutGap as RunOfShowItem; + // Return item without calculatedGap property + return { + id: item.id, + type: item.type, + itemNumber: itemCount.toString(), + startTime: formatSecondsToTime(newStartTime), + highlightColor: item.highlightColor, + preset: item.preset, + duration: item.duration, + privateNotes: item.privateNotes, + productionNotes: item.productionNotes, + audio: item.audio, + video: item.video, + lights: item.lights, + // Include custom column values + ...Object.keys(item) + .filter( + (key) => + ![ + "id", + "type", + "itemNumber", + "startTime", + "highlightColor", + "preset", + "duration", + "privateNotes", + "productionNotes", + "audio", + "video", + "lights", + "calculatedGap", + "headerTitle", + ].includes(key), + ) + .reduce( + (acc, key) => { + acc[key] = item[key as keyof RunOfShowItem]; + return acc; + }, + {} as Record, + ), + } as RunOfShowItem; }); setRunOfShow({ ...runOfShow, items: recalculatedItems }); From 9f687a5d6f724ddc87f7a6bd0ec0cb68ebdbe843 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Tue, 7 Oct 2025 12:19:07 -0500 Subject: [PATCH 5/5] fix: correct LLM import instructions for absolute start times MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated ImportShowFlowModal.tsx to clarify that start times should be actual wall-clock times when items/headers occur in the show, not sequential times starting from 00:00:00. Changes: - Field specification: Clarified startTime is absolute show time (e.g., "19:30:00" for 7:30 PM) - Conversion guidelines: Updated time calculation to use actual wall-clock times - Examples: Changed from 00:00:00/00:30:00 to realistic times (18:30:00/19:00:00) - Added explicit note that startTime is NOT relative to show start This fixes confusion where LLMs would generate shows with all times starting at midnight instead of actual show times. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/components/ImportShowFlowModal.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ImportShowFlowModal.tsx b/apps/web/src/components/ImportShowFlowModal.tsx index fea9bb7..8ea71b5 100644 --- a/apps/web/src/components/ImportShowFlowModal.tsx +++ b/apps/web/src/components/ImportShowFlowModal.tsx @@ -50,7 +50,7 @@ Each item in the \`items\` array must follow this format: "id": "unique-id-here", "type": "item", "itemNumber": "1", - "startTime": "00:00:00", + "startTime": "19:00:00", // Actual show time (e.g., 7:00 PM) "preset": "Scene or preset name", "duration": "00:30", "privateNotes": "Internal notes not shown to audience", @@ -68,7 +68,7 @@ Each item in the \`items\` array must follow this format: "id": "unique-id-here", "type": "header", "itemNumber": "", // Leave empty for headers - "startTime": "00:00:00", // Optional for headers + "startTime": "19:00:00", // Actual show time when section starts "headerTitle": "Section Name", "highlightColor": "#0000FF" // Optional, but recommended for visual organization } @@ -79,7 +79,7 @@ Each item in the \`items\` array must follow this format: - **id**: Generate a unique identifier for each item (e.g., "item-1", "header-1", or UUID) - **type**: Either "item" for regular entries or "header" for section dividers - **itemNumber**: Sequential number for items (string). Leave empty ("") for headers -- **startTime**: Time in HH:MM:SS format (e.g., "00:15:30"). Calculate based on previous items +- **startTime**: The actual absolute time when this item/header starts in HH:MM:SS format (e.g., "19:30:00" for 7:30 PM). This is NOT relative to show start - it's the real wall-clock time - **duration**: Duration in MM:SS format (e.g., "05:30" for 5 minutes 30 seconds) - **preset**: The scene, preset, or segment name (descriptive and clear) - **privateNotes**: Internal notes for operators (technical details, warnings, reminders) @@ -116,7 +116,7 @@ For specialized productions, add custom fields: ## Conversion Guidelines: -1. **Time Calculation**: Start at "00:00:00" and calculate each subsequent startTime by adding the previous item's duration +1. **Time Calculation**: Use actual absolute wall-clock times for all start times. If the show starts at 7:00 PM (19:00), the first item should be "19:00:00", not "00:00:00". Calculate each subsequent startTime by adding the previous item's duration to its start time, plus any intentional gaps/buffers 2. **Section Organization**: Use headers to break the show into logical sections 3. **Color Coding**: Use highlight colors to help operators quickly identify different types of content 4. **Technical Details**: Include specific technical information that operators would need: @@ -148,7 +148,7 @@ Q&A session "id": "header-1", "type": "header", "itemNumber": "", - "startTime": "00:00:00", + "startTime": "18:30:00", "headerTitle": "Pre-Show", "highlightColor": "#4A90E2" }, @@ -156,7 +156,7 @@ Q&A session "id": "item-1", "type": "item", "itemNumber": "1", - "startTime": "00:00:00", + "startTime": "18:30:00", "preset": "Walk-in Music & House Open", "duration": "30:00", "privateNotes": "Loop playlist, check all wireless mics", @@ -169,7 +169,7 @@ Q&A session "id": "item-2", "type": "item", "itemNumber": "2", - "startTime": "00:30:00", + "startTime": "19:00:00", "preset": "CEO Opening Speech", "duration": "10:00", "privateNotes": "CEO enters from stage left, mic check complete",