```html

Fixing the Booking Calendar Race Condition on SailJada: A Multi-Page Synchronization Fix

What Was Done

We identified and resolved a critical race condition in the booking flow across the SailJada website where users could attempt to book sailing slots that were already unavailable. The issue occurred because the booking calendar became interactive before availability data finished loading from external sources. We implemented a synchronization mechanism in the jadaOpenBook() function that blocks calendar interaction until all required data is loaded, then deployed the fix across all 22 HTML pages in the site.

The Problem: Race Condition in Booking Flow

The SailJada website integrates with multiple external booking systems (GetMyBoat, Viator, and internal calendar APIs) to display real-time availability. The original booking flow had this sequence:

  1. User clicks "Book Now" button, triggering jadaOpenBook()
  2. Function immediately renders the booking modal with the interactive calendar UI
  3. Asynchronous fetch requests for availability data begin in the background
  4. User can interact with the calendar before availability data returns
  5. User selects a date that may already be booked

This created a window of vulnerability where user selections weren't validated against actual availability—a classic race condition where concurrent async operations created unpredictable state.

Technical Implementation

Root Cause Analysis

We located the vulnerable code in /Users/cb/Documents/repos/sites/sailjada.com/index.html and related pages. The jadaOpenBook() function was rendering the modal DOM element synchronously while firing asynchronous fetch requests. The modal's event listeners were immediately active, but the underlying data they depended on wasn't guaranteed to be loaded.

The Fix: Promise-Based Synchronization

We modified the jadaOpenBook() function to implement explicit promise-based waiting:

function jadaOpenBook() {
  // Show loading state immediately
  const modalOverlay = document.querySelector('.jada-modal-overlay');
  modalOverlay.classList.add('jada-loading');
  
  // Create promises for each data dependency
  const availabilityPromise = fetch('/api/availability')
    .then(r => r.json())
    .then(data => {
      window.jadaBookingState.availability = data;
      return data;
    });
  
  const calendarPromise = fetch('/api/calendar-slots')
    .then(r => r.json())
    .then(data => {
      window.jadaBookingState.calendar = data;
      return data;
    });
  
  // Wait for ALL data before enabling interaction
  Promise.all([availabilityPromise, calendarPromise])
    .then(() => {
      // Now render the calendar with validated data
      renderBookingCalendar(window.jadaBookingState);
      modalOverlay.classList.remove('jada-loading');
      modalOverlay.classList.add('jada-ready');
    })
    .catch(error => {
      modalOverlay.classList.add('jada-error');
      console.error('Booking data load failed:', error);
      showErrorMessage('Unable to load availability. Please try again.');
    });
}

Key improvements:

  • Explicit Promise.all(): Uses Promise.all() to wait for all async operations simultaneously, with proper error handling
  • Loading State Management: DOM classes (jada-loading, jada-ready, jada-error) indicate UI state, with CSS rules preventing interaction during loading
  • Centralized State: All booking state stored in window.jadaBookingState object, eliminating implicit dependencies
  • Validation Before Render: Calendar only renders after all data is loaded and validated

CSS Guard Rails

We added CSS to prevent user interaction during loading:

.jada-modal-overlay.jada-loading {
  pointer-events: none;
  opacity: 0.6;
}

.jada-modal-overlay.jada-loading .jadaCalendar {
  cursor: not-allowed;
}

.jada-modal-overlay.jada-ready {
  pointer-events: auto;
  opacity: 1;
}

Multi-Page Deployment Strategy

The fix needed to be applied across all pages in the site. We identified files containing booking integration:

  • /index.html (main homepage)
  • /about/index.html
  • /contact/index.html
  • /sd-sailing-calendar/index.html
  • 18 additional pages with booking CTAs

Rather than manually editing each file, we created a Python script to apply the transformation consistently across all 22 HTML files:

#!/usr/bin/env python3
import os
import re
import glob

pattern = r''
replacement = '''
'''

for filepath in glob.glob('/Users/cb/Documents/repos/sites/sailjada.com/**/*.html', recursive=True):
  with open(filepath, 'r') as f:
    content = f.read()
  
  if 'jadaOpenBook' in content:
    updated = re.sub(pattern, replacement, content, flags=re.DOTALL)
    with open(filepath, 'w') as f:
      f.write(updated)
    print(f'Updated: {filepath}')

This approach:

  • Extracted booking logic into /assets/booking-fix.js (single source of truth)
  • Injected initialization on all pages consistently
  • Ensured version control tracked exact changes across all files

Staging Deployment

Following the staging rule, we deployed to the staging bucket before production:

aws s3 sync /Users/cb/Documents/repos/sites/sailjada.com \
  s3://queenofsandiego.com/_staging/sailjada/ \
  --delete \
  --exclude ".git/*" \
  --exclude "node_modules/*"

# CloudFront invalidation for staging
aws cloudfront create-invalidation \
  --distribution-id E1ABC2DEF3GHIJ \
  --paths "/_staging/sailjada/*"

Staging URL: https://queenofsandiego.com/_staging/sailjada/

Why This Approach?

  • Promise-Based: Modern async/await would be cleaner, but Promise.all() maintains compatibility with existing codebase
  • CSS State Classes: Leverages cascade to prevent user interaction—more reliable than JavaScript-only guards
  • Centralized State Object: Easier to debug than scattered variables; facilitates future analytics and error tracking
  • Script Extraction: Single source of truth in